blob: fb363be27501389fbb1c2218eeb92ae6cfdc1120 [file] [log] [blame]
#pragma once
#include "app.hpp"
#include "async_resp.hpp"
#include "websocket.hpp"
#include <sys/socket.h>
#include <boost/asio/local/stream_protocol.hpp>
#include <boost/container/flat_map.hpp>
namespace crow
{
namespace obmc_console
{
// Update this value each time we add new console route.
static constexpr const uint maxSessions = 32;
class ConsoleHandler : public std::enable_shared_from_this<ConsoleHandler>
{
public:
ConsoleHandler(boost::asio::io_context& ioc,
crow::websocket::Connection& connIn) :
hostSocket(ioc),
conn(connIn)
{}
~ConsoleHandler() = default;
ConsoleHandler(const ConsoleHandler&) = delete;
ConsoleHandler(ConsoleHandler&&) = delete;
ConsoleHandler& operator=(const ConsoleHandler&) = delete;
ConsoleHandler& operator=(ConsoleHandler&&) = delete;
void doWrite()
{
if (doingWrite)
{
BMCWEB_LOG_DEBUG("Already writing. Bailing out");
return;
}
if (inputBuffer.empty())
{
BMCWEB_LOG_DEBUG("Outbuffer empty. Bailing out");
return;
}
doingWrite = true;
hostSocket.async_write_some(
boost::asio::buffer(inputBuffer.data(), inputBuffer.size()),
[weak(weak_from_this())](const boost::beast::error_code& ec,
std::size_t bytesWritten) {
std::shared_ptr<ConsoleHandler> self = weak.lock();
if (self == nullptr)
{
return;
}
self->doingWrite = false;
self->inputBuffer.erase(0, bytesWritten);
if (ec == boost::asio::error::eof)
{
self->conn.close("Error in reading to host port");
return;
}
if (ec)
{
BMCWEB_LOG_ERROR("Error in host serial write {}", ec.message());
return;
}
self->doWrite();
});
}
static void afterSendEx(const std::weak_ptr<ConsoleHandler>& weak)
{
std::shared_ptr<ConsoleHandler> self = weak.lock();
if (self == nullptr)
{
return;
}
self->doRead();
}
void doRead()
{
BMCWEB_LOG_DEBUG("Reading from socket");
hostSocket.async_read_some(
boost::asio::buffer(outputBuffer),
[this, weakSelf(weak_from_this())](
const boost::system::error_code& ec, std::size_t bytesRead) {
BMCWEB_LOG_DEBUG("read done. Read {} bytes", bytesRead);
std::shared_ptr<ConsoleHandler> self = weakSelf.lock();
if (self == nullptr)
{
return;
}
if (ec)
{
BMCWEB_LOG_ERROR("Couldn't read from host serial port: {}",
ec.message());
conn.close("Error connecting to host port");
return;
}
std::string_view payload(outputBuffer.data(), bytesRead);
self->conn.sendEx(crow::websocket::MessageType::Binary, payload,
std::bind_front(afterSendEx, weak_from_this()));
});
}
bool connect(int fd)
{
boost::system::error_code ec;
boost::asio::local::stream_protocol proto;
hostSocket.assign(proto, fd, ec);
if (ec)
{
BMCWEB_LOG_ERROR(
"Failed to assign the DBUS socket Socket assign error: {}",
ec.message());
return false;
}
conn.resumeRead();
doWrite();
doRead();
return true;
}
boost::asio::local::stream_protocol::socket hostSocket;
std::array<char, 4096> outputBuffer{};
std::string inputBuffer;
bool doingWrite = false;
crow::websocket::Connection& conn;
};
using ObmcConsoleMap = boost::container::flat_map<
crow::websocket::Connection*, std::shared_ptr<ConsoleHandler>, std::less<>,
std::vector<std::pair<crow::websocket::Connection*,
std::shared_ptr<ConsoleHandler>>>>;
inline ObmcConsoleMap& getConsoleHandlerMap()
{
static ObmcConsoleMap map;
return map;
}
// Remove connection from the connection map and if connection map is empty
// then remove the handler from handlers map.
inline void onClose(crow::websocket::Connection& conn, const std::string& err)
{
BMCWEB_LOG_INFO("Closing websocket. Reason: {}", err);
auto iter = getConsoleHandlerMap().find(&conn);
if (iter == getConsoleHandlerMap().end())
{
BMCWEB_LOG_CRITICAL("Unable to find connection {}", logPtr(&conn));
return;
}
BMCWEB_LOG_DEBUG("Remove connection {} from obmc console", logPtr(&conn));
// Removed last connection so remove the path
getConsoleHandlerMap().erase(iter);
}
inline void connectConsoleSocket(crow::websocket::Connection& conn,
const boost::system::error_code& ec,
const sdbusplus::message::unix_fd& unixfd)
{
if (ec)
{
BMCWEB_LOG_ERROR(
"Failed to call console Connect() method DBUS error: {}",
ec.message());
conn.close("Failed to connect");
return;
}
// Look up the handler
auto iter = getConsoleHandlerMap().find(&conn);
if (iter == getConsoleHandlerMap().end())
{
BMCWEB_LOG_ERROR("Connection was already closed");
return;
}
int fd = dup(unixfd);
if (fd == -1)
{
BMCWEB_LOG_ERROR("Failed to dup the DBUS unixfd error: {}",
strerror(errno));
conn.close("Internal error");
return;
}
BMCWEB_LOG_DEBUG("Console duped FD: {}", fd);
if (!iter->second->connect(fd))
{
close(fd);
conn.close("Internal Error");
}
}
inline void
processConsoleObject(crow::websocket::Connection& conn,
const std::string& consoleObjPath,
const boost::system::error_code& ec,
const ::dbus::utility::MapperGetObject& objInfo)
{
// Look up the handler
auto iter = getConsoleHandlerMap().find(&conn);
if (iter == getConsoleHandlerMap().end())
{
BMCWEB_LOG_ERROR("Connection was already closed");
return;
}
if (ec)
{
BMCWEB_LOG_WARNING("getDbusObject() for consoles failed. DBUS error:{}",
ec.message());
conn.close("getDbusObject() for consoles failed.");
return;
}
const auto valueIface = objInfo.begin();
if (valueIface == objInfo.end())
{
BMCWEB_LOG_WARNING("getDbusObject() returned unexpected size: {}",
objInfo.size());
conn.close("getDbusObject() returned unexpected size");
return;
}
const std::string& consoleService = valueIface->first;
BMCWEB_LOG_DEBUG("Looking up unixFD for Service {} Path {}", consoleService,
consoleObjPath);
// Call Connect() method to get the unix FD
crow::connections::systemBus->async_method_call(
[&conn](const boost::system::error_code& ec1,
const sdbusplus::message::unix_fd& unixfd) {
connectConsoleSocket(conn, ec1, unixfd);
},
consoleService, consoleObjPath, "xyz.openbmc_project.Console.Access",
"Connect");
}
// Query consoles from DBUS and find the matching to the
// rules string.
inline void onOpen(crow::websocket::Connection& conn)
{
std::string consoleLeaf;
BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn));
if (getConsoleHandlerMap().size() >= maxSessions)
{
conn.close("Max sessions are already connected");
return;
}
std::shared_ptr<ConsoleHandler> handler =
std::make_shared<ConsoleHandler>(conn.getIoContext(), conn);
getConsoleHandlerMap().emplace(&conn, handler);
conn.deferRead();
// Keep old path for backward compatibility
if (conn.url().path() == "/console0")
{
consoleLeaf = "default";
}
else
{
// Get the console id from console router path and prepare the console
// object path and console service.
consoleLeaf = conn.url().segments().back();
}
std::string consolePath =
sdbusplus::message::object_path("/xyz/openbmc_project/console") /
consoleLeaf;
BMCWEB_LOG_DEBUG("Console Object path = {} Request target = {}",
consolePath, conn.url().path());
// mapper call lambda
constexpr std::array<std::string_view, 1> interfaces = {
"xyz.openbmc_project.Console.Access"};
dbus::utility::getDbusObject(
consolePath, interfaces,
[&conn, consolePath](const boost::system::error_code& ec,
const ::dbus::utility::MapperGetObject& objInfo) {
processConsoleObject(conn, consolePath, ec, objInfo);
});
}
inline void onMessage(crow::websocket::Connection& conn,
const std::string& data, bool /*isBinary*/)
{
auto handler = getConsoleHandlerMap().find(&conn);
if (handler == getConsoleHandlerMap().end())
{
BMCWEB_LOG_CRITICAL("Unable to find connection {}", logPtr(&conn));
return;
}
handler->second->inputBuffer += data;
handler->second->doWrite();
}
inline void requestRoutes(App& app)
{
BMCWEB_ROUTE(app, "/console0")
.privileges({{"OpenBMCHostConsole"}})
.websocket()
.onopen(onOpen)
.onclose(onClose)
.onmessage(onMessage);
BMCWEB_ROUTE(app, "/console/<str>")
.privileges({{"OpenBMCHostConsole"}})
.websocket()
.onopen(onOpen)
.onclose(onClose)
.onmessage(onMessage);
}
} // namespace obmc_console
} // namespace crow