blob: 80ebd3dab752c29e94d13f40968136f6e541f9ef [file] [log] [blame]
#ifndef THIRD_PARTY_GBMCWEB_HTTP_HTTP_CONNECTION_H_
#define THIRD_PARTY_GBMCWEB_HTTP_HTTP_CONNECTION_H_
#include <array>
#include <charconv>
#include <chrono> // NOLINT
#include <cstddef>
#include <cstdint>
#include <filesystem> // NOLINT
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <system_error> // NOLINT
#include <tuple>
#include <utility>
#include "boost/algorithm/string/predicate.hpp" // NOLINT
#include "boost/asio/io_context.hpp" // NOLINT
#include "boost/asio/io_context_strand.hpp" // NOLINT
#include "boost/asio/ip/tcp.hpp" // NOLINT
#include "boost/asio/socket_base.hpp" // NOLINT
#include "boost/asio/ssl/stream.hpp" // NOLINT
#include "boost/asio/steady_timer.hpp" // NOLINT
#include "boost/beast/core/flat_static_buffer.hpp" // NOLINT
#include "boost/beast/http/error.hpp" // NOLINT
#include "boost/beast/http/parser.hpp" // NOLINT
#include "boost/beast/http/read.hpp" // NOLINT
#include "boost/beast/http/serializer.hpp" // NOLINT
#include "boost/beast/http/write.hpp" // NOLINT
#include "boost/beast/ssl/ssl_stream.hpp" // NOLINT
#include "boost/beast/websocket.hpp" // NOLINT
#include "bmcweb_config.h"
#include "http_request.hpp"
#include "http_response.hpp"
#include "logging.hpp"
#include "mutual_tls.hpp"
#include "async_resp.hpp"
#include "authentication.hpp"
#include "forward_unauthorized.hpp"
#include "http_utility.hpp"
#include "json_html_serializer.hpp"
#include "security_headers.hpp"
#include "sessions.hpp"
#include "ssl_key_handler.hpp"
#include <nlohmann/json.hpp>
namespace crow {
inline void prettyPrintJson(crow::Response& res) {
json_html_util::dumpHtml(res.body(), res.jsonValue);
res.addHeader(boost::beast::http::field::content_type,
"text/html;charset=UTF-8");
}
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static int connectionCount = 0;
// request body limit size set by the bmcwebHttpReqBodyLimitMb option
constexpr uint64_t httpReqBodyLimit =
1024UL * 1024UL * bmcwebHttpReqBodyLimitMb;
constexpr uint64_t loggedOutPostBodyLimit = 4096;
constexpr uint32_t httpHeaderLimit = 8192;
template <typename Adaptor, typename Handler>
class Connection
: public std::enable_shared_from_this<Connection<Adaptor, Handler>> {
using self_type = Connection<Adaptor, Handler>;
public:
Connection(Handler* handlerIn, boost::asio::steady_timer&& timerIn,
std::function<std::string()>& getCachedDateStrF, Adaptor adaptorIn)
: adaptor(std::move(adaptorIn)),
handler(handlerIn),
timer(std::move(timerIn)),
getCachedDateStr(getCachedDateStrF) {
parser.emplace(std::piecewise_construct, std::make_tuple());
parser->body_limit(httpReqBodyLimit);
parser->header_limit(httpHeaderLimit);
#ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
prepareMutualTls();
#endif // BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
connectionCount++;
BMCWEB_LOG_DEBUG << this << " Connection open, total " << connectionCount;
}
~Connection() {
res.setCompleteRequestHandler(nullptr);
cancelDeadlineTimer();
connectionCount--;
BMCWEB_LOG_DEBUG << this << " Connection closed, total " << connectionCount;
}
Connection(const Connection&) = delete;
Connection(Connection&&) = delete;
Connection& operator=(const Connection&) = delete;
Connection& operator=(Connection&&) = delete;
bool tlsVerifyCallback(bool preverified,
boost::asio::ssl::verify_context& ctx) {
// We always return true to allow full auth flow for resources that
// don't require auth
if (preverified) {
mtlsSession = verifyMtlsUser(req->ipAddress, ctx);
if (mtlsSession) {
BMCWEB_LOG_DEBUG << this << " Generating TLS session: "
<< mtlsSession->uniqueId;
}
}
return true;
}
void prepareMutualTls() {
std::error_code error;
std::filesystem::path ca_path(ensuressl::trustStorePath);
auto ca_available = !std::filesystem::is_empty(ca_path, error);
ca_available = ca_available && !error;
if (ca_available && persistent_data::SessionStore::getInstance()
.getAuthMethodsConfig()
.tls) {
adaptor.set_verify_mode(boost::asio::ssl::verify_peer);
std::string id = "bmcweb";
const char* c_str = id.c_str();
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
const auto* id_c = reinterpret_cast<const unsigned char*>(c_str);
int ret =
SSL_set_session_id_context(adaptor.native_handle(), id_c,
static_cast<unsigned int>(id.length()));
if (ret == 0) {
BMCWEB_LOG_ERROR << this << " failed to set SSL id";
}
}
adaptor.set_verify_callback(
std::bind_front(&self_type::tlsVerifyCallback, this));
}
Adaptor& socket() { return adaptor; }
void start() {
if (connectionCount >= 100) {
BMCWEB_LOG_CRITICAL << this << "Max connection count exceeded.";
return;
}
startDeadline();
// TODO(ed) Abstract this to a more clever class with the idea of an
// asynchronous "start"
if constexpr (std::is_same_v<Adaptor, boost::beast::ssl_stream<
boost::asio::ip::tcp::socket>>) {
adaptor.async_handshake(boost::asio::ssl::stream_base::server,
[this, self(shared_from_this())](
const boost::system::error_code& ec) {
if (ec) {
return;
}
doReadHeaders();
});
} else {
doReadHeaders();
}
}
void handle() {
std::error_code req_ec;
crow::Request& this_req = req.emplace(parser->release(), req_ec);
if (req_ec) {
BMCWEB_LOG_DEBUG << "Request failed to construct" << req_ec;
res.result(boost::beast::http::status::bad_request);
completeRequest(res);
return;
}
int64_t max_age_sec = 0;
std::string_view max_age_sec_str =
this_req.getHeaderValue(bmcweb::BMCWEB_HINT_MAX_AGE_SEC);
if (!max_age_sec_str.empty()) {
std::from_chars(max_age_sec_str.data(),
max_age_sec_str.data() + max_age_sec_str.size(),
max_age_sec);
}
BMCWEB_LOG_ALWAYS << "=> Connection: handle: target: " << this_req.target()
<< " max_age_sec_str: " << max_age_sec_str
<< " max_age_sec: " << max_age_sec;
this_req.session = userSession;
// Fetch the client IP address
readClientIp();
// Check for HTTP version 1.1.
if (this_req.version() == 11) {
if (this_req.getHeaderValue(boost::beast::http::field::host).empty()) {
res.result(boost::beast::http::status::bad_request);
completeRequest(res);
return;
}
}
BMCWEB_LOG_INFO << "Request: "
<< " " << this << " HTTP/" << this_req.version() / 10 << "."
<< this_req.version() % 10 << ' ' << this_req.methodString()
<< " " << this_req.target() << " "
<< this_req.ipAddress.to_string();
std::string uri_string = static_cast<std::string>(this_req.target());
res.requestStartTime(uri_string);
res.startTimetrace("BMCWEB");
res.isAliveHelper = [this]() -> bool { return isAlive(); };
this_req.ioService = static_cast<decltype(this_req.ioService)>(
&adaptor.get_executor().context());
if (res.completed) {
completeRequest(res);
return;
}
keepAlive = this_req.keepAlive();
#ifndef BMCWEB_INSECURE_DISABLE_AUTHX
if (!crow::authentication::isOnAllowlist(req->url().buffer(),
req->method()) &&
this_req.session == nullptr) {
BMCWEB_LOG_WARNING << "Authentication failed";
forward_unauthorized::sendUnauthorized(
req->url().encoded_path(), req->getHeaderValue("X-Requested-With"),
req->getHeaderValue("Accept"), res);
completeRequest(res);
return;
}
#endif // BMCWEB_INSECURE_DISABLE_AUTHX
auto async_resp = std::make_shared<bmcweb::AsyncResp>(nullptr);
async_resp->hintMaxAgeSec =
max_age_sec; // TODO: Remove after g3 managedStore dependency merges
async_resp->hintCacheMaxAgeSec = max_age_sec;
BMCWEB_LOG_DEBUG << "Setting completion handler: " << " hintMaxAgeSec: "
<< async_resp->hintCacheMaxAgeSec.value();
async_resp->res.setCompleteRequestHandler(
[self(shared_from_this())](crow::Response& thisRes) {
self->completeRequest(thisRes);
});
if (this_req.isUpgrade() &&
boost::iequals(
this_req.getHeaderValue(boost::beast::http::field::upgrade),
"websocket")) {
async_resp->res.setCompleteRequestHandler(
[self(shared_from_this())](crow::Response& thisRes) {
if (thisRes.result() != boost::beast::http::status::ok) {
// When any error occurs before handle upgradation,
// the result in response will be set to respective
// error. By default the Result will be OK (200),
// which implies successful handle upgrade. Response
// needs to be sent over this connection only on
// failure.
self->completeRequest(thisRes);
return;
}
});
handler->handleUpgrade(this_req, async_resp, std::move(adaptor));
return;
}
std::string_view expected =
req->getHeaderValue(boost::beast::http::field::if_none_match);
if (!expected.empty()) {
res.setExpectedHash(expected);
}
async_resp->res.startTimetrace("LOCAL");
// Only post to strand for GET requests.
if (this_req.method() == boost::beast::http::verb::get) {
auto strand = std::make_shared<boost::asio::io_context::strand>(
*this_req.ioService);
async_resp->strand_ = strand;
strand->post([this, this_req{std::move(this_req)},
async_resp{std::move(async_resp)}]() mutable {
handler->handle(this_req, async_resp);
async_resp->res.endTimetrace("LOCAL");
});
return;
}
handler->handle(this_req, async_resp);
async_resp->res.endTimetrace("LOCAL");
}
bool isAlive() {
if constexpr (std::is_same_v<Adaptor, boost::beast::ssl_stream<
boost::asio::ip::tcp::socket>>) {
return adaptor.next_layer().is_open();
} else {
return adaptor.is_open();
}
}
// Set the socket to use a keepAlive. ONLY CALL THIS DURING SOCKET ACCEPT
void setKeepAlive() {
// Sets SO_KEEPALIVE. For more info see
// https://beta.boost.org/doc/libs/1_82_0/doc/html/boost_asio/reference/socket_base/keep_alive.html
boost::asio::socket_base::keep_alive option(true);
if constexpr (std::is_same_v<Adaptor, boost::beast::ssl_stream<
boost::asio::ip::tcp::socket>>) {
adaptor.next_layer().set_option(option);
} else {
adaptor.set_option(option);
}
}
void close() {
if constexpr (std::is_same_v<Adaptor, boost::beast::ssl_stream<
boost::asio::ip::tcp::socket>>) {
adaptor.next_layer().close();
if (mtlsSession != nullptr) {
BMCWEB_LOG_DEBUG << this
<< " Removing TLS session: " << mtlsSession->uniqueId;
persistent_data::SessionStore::getInstance().removeSession(mtlsSession);
}
} else {
adaptor.close();
}
}
void completeRequest(crow::Response& thisRes) {
if (!req) {
return;
}
res = std::move(thisRes);
res.keepAlive(keepAlive);
BMCWEB_LOG_INFO << "Response: " << this << ' ' << req->url().encoded_path()
<< ' ' << res.resultInt() << " keepalive=" << keepAlive;
addSecurityHeaders(*req, res);
crow::authentication::cleanupTempSession(*req);
if (!isAlive()) {
// BMCWEB_LOG_DEBUG << this << " delete (socket is closed) " <<
// isReading
// << ' ' << isWriting;
// delete this;
// delete lambda with self shared_ptr
// to enable connection destruction
res.setCompleteRequestHandler(nullptr);
return;
}
res.setHashAndHandleNotModified();
if (res.body().empty() && !res.jsonValue.empty()) {
using http_helpers::ContentType;
std::array<ContentType, 3> allowed{ContentType::CBOR, ContentType::JSON,
ContentType::HTML};
ContentType prefered =
getPreferedContentType(req->getHeaderValue("Accept"), allowed);
if (prefered == ContentType::HTML) {
prettyPrintJson(res);
} else if (prefered == ContentType::CBOR) {
res.addHeader(boost::beast::http::field::content_type,
"application/cbor");
nlohmann::json::to_cbor(res.jsonValue, res.body());
} else {
// Technically prefered could also be NoMatch here, but we'd
// like to default to something rather than return 400 for
// backward compatibility.
res.addHeader(boost::beast::http::field::content_type,
"application/json");
res.body() = res.jsonValue.dump(
2, ' ', true, nlohmann::json::error_handler_t::replace);
}
}
if (res.resultInt() >= 400 && res.body().empty()) {
res.body() = std::string(res.reason());
}
if (res.result() == boost::beast::http::status::no_content) {
// Boost beast throws if content is provided on a no-content
// response. Ideally, this would never happen, but in the case that
// it does, we don't want to throw.
BMCWEB_LOG_CRITICAL
<< this << " Response content provided but code was no-content";
res.body().clear();
}
res.addHeader(boost::beast::http::field::date, getCachedDateStr());
res.endTimetrace("BMCWEB");
res.requestEndTime();
res.updateTraceInManagedStore();
doWrite(res);
// delete lambda with self shared_ptr
// to enable connection destruction
res.setCompleteRequestHandler(nullptr);
}
void readClientIp() {
boost::asio::ip::address ip;
boost::system::error_code ec = getClientIp(ip);
if (ec) {
return;
}
req->ipAddress = ip;
}
boost::system::error_code getClientIp(boost::asio::ip::address& ip) {
boost::system::error_code ec;
BMCWEB_LOG_DEBUG << "Fetch the client IP address";
boost::asio::ip::tcp::endpoint endpoint =
boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec);
if (ec) {
// If remote endpoint fails keep going. "ClientOriginIPAddress"
// will be empty.
BMCWEB_LOG_ERROR << "Failed to get the client's IP Address. ec : " << ec;
return ec;
}
ip = endpoint.address();
return ec;
}
private:
void doReadHeaders() {
BMCWEB_LOG_DEBUG << this << " doReadHeaders";
// Clean up any previous Connection.
boost::beast::http::async_read_header(
adaptor, buffer, *parser,
[this, self(shared_from_this())](const boost::system::error_code& ec,
std::size_t bytesTransferred) {
BMCWEB_LOG_DEBUG << this << " async_read_header " << bytesTransferred
<< " Bytes";
bool errorWhileReading = false;
if (ec) {
errorWhileReading = true;
if (ec == boost::beast::http::error::end_of_stream) {
BMCWEB_LOG_WARNING << this
<< " Error while reading: " << ec.message();
} else {
BMCWEB_LOG_ERROR << this
<< " Error while reading: " << ec.message();
}
} else {
// if the adaptor isn't open anymore, and wasn't handed to a
// websocket, treat as an error
if (!isAlive() &&
!boost::beast::websocket::is_upgrade(parser->get())) {
errorWhileReading = true;
}
}
cancelDeadlineTimer();
if (errorWhileReading) {
close();
BMCWEB_LOG_DEBUG << this << " from read(1)";
return;
}
readClientIp();
boost::asio::ip::address ip;
if (getClientIp(ip)) {
BMCWEB_LOG_DEBUG << "Unable to get client IP";
}
#ifndef BMCWEB_INSECURE_DISABLE_AUTHX
boost::beast::http::verb method = parser->get().method();
userSession = crow::authentication::authenticate(
ip, res, method, parser->get().base(), mtlsSession);
bool loggedIn = userSession != nullptr;
if (!loggedIn) {
const boost::optional<uint64_t> contentLength =
parser->content_length();
if (contentLength && *contentLength > loggedOutPostBodyLimit) {
BMCWEB_LOG_DEBUG << "Content length greater than limit "
<< *contentLength;
close();
return;
}
BMCWEB_LOG_DEBUG << "Starting quick deadline";
}
#endif // BMCWEB_INSECURE_DISABLE_AUTHX
if (parser->is_done()) {
handle();
return;
}
doRead();
});
}
void doRead() {
BMCWEB_LOG_DEBUG << this << " doRead";
startDeadline();
boost::beast::http::async_read_some(
adaptor, buffer, *parser,
[this, self(shared_from_this())](const boost::system::error_code& ec,
std::size_t bytesTransferred) {
BMCWEB_LOG_DEBUG << this << " async_read_some " << bytesTransferred
<< " Bytes";
if (ec) {
BMCWEB_LOG_ERROR << this
<< " Error while reading: " << ec.message();
close();
BMCWEB_LOG_DEBUG << this << " from read(1)";
return;
}
// If the user is logged in, allow them to send files incrementally
// one piece at a time. If authentication is disabled then there is
// no user session hence always allow to send one piece at a time.
if (userSession != nullptr) {
cancelDeadlineTimer();
}
if (!parser->is_done()) {
doRead();
return;
}
cancelDeadlineTimer();
handle();
});
}
void doWrite(crow::Response& thisRes) {
BMCWEB_LOG_DEBUG << this << " doWrite";
thisRes.preparePayload();
serializer.emplace(*thisRes.stringResponse);
startDeadline();
boost::beast::http::async_write(
adaptor, *serializer,
[this, self(shared_from_this())](const boost::system::error_code& ec,
std::size_t bytesTransferred) {
BMCWEB_LOG_DEBUG << this << " async_write " << bytesTransferred
<< " bytes";
cancelDeadlineTimer();
if (ec) {
BMCWEB_LOG_DEBUG << this << " from write(2)";
return;
}
if (!keepAlive) {
close();
BMCWEB_LOG_DEBUG << this << " from write(1)";
return;
}
serializer.reset();
BMCWEB_LOG_DEBUG << this << " Clearing response";
res.clear();
parser.emplace(std::piecewise_construct, std::make_tuple());
parser->body_limit(httpReqBodyLimit); // reset body limit for
// newly created parser
buffer.consume(buffer.size());
userSession = nullptr;
// Destroy the Request via the std::optional
req.reset();
doReadHeaders();
});
}
void cancelDeadlineTimer() { timer.cancel(); }
void startDeadline() {
// Timer is already started so no further action is required.
if (timerStarted) {
return;
}
std::chrono::seconds timeout(15);
std::weak_ptr<Connection<Adaptor, Handler>> weak_self = weak_from_this();
timer.expires_after(timeout);
timer.async_wait([weak_self](const boost::system::error_code ec) {
// Note, we are ignoring other types of errors here; If the timer
// failed for any reason, we should still close the connection
std::shared_ptr<Connection<Adaptor, Handler>> self = weak_self.lock();
if (!self) {
BMCWEB_LOG_CRITICAL << self << " Failed to capture connection";
return;
}
self->timerStarted = false;
if (ec == boost::asio::error::operation_aborted) {
// Canceled wait means the path succeeded.
return;
}
if (ec) {
BMCWEB_LOG_CRITICAL << self << " timer failed " << ec;
}
BMCWEB_LOG_WARNING << self << "Connection timed out, closing";
self->close();
});
timerStarted = true;
BMCWEB_LOG_DEBUG << this << " timer started";
}
Adaptor adaptor; // NOLINT
Handler* handler; // NOLINT
// Making this a std::optional allows it to be efficiently destroyed and
// re-created on Connection reset
std::optional<
boost::beast::http::request_parser<boost::beast::http::string_body>>
parser; // NOLINT
boost::beast::flat_static_buffer<8192> buffer; // NOLINT
std::optional<
boost::beast::http::response_serializer<boost::beast::http::string_body>>
serializer; // NOLINT
std::optional<crow::Request> req; // NOLINT
crow::Response res; // NOLINT
std::shared_ptr<persistent_data::UserSession> userSession; // NOLINT
std::shared_ptr<persistent_data::UserSession> mtlsSession; // NOLINT
boost::asio::steady_timer timer; // NOLINT
bool keepAlive = true; // NOLINT
bool timerStarted = false; // NOLINT
std::function<std::string()>& getCachedDateStr; // NOLINT
using std::enable_shared_from_this<
Connection<Adaptor, Handler>>::shared_from_this;
using std::enable_shared_from_this<
Connection<Adaptor, Handler>>::weak_from_this;
};
} // namespace crow
#endif // THIRD_PARTY_GBMCWEB_HTTP_HTTP_CONNECTION_H_