#pragma once #include "app.hpp" #include "dbus_utility.hpp" #include "privileges.hpp" #include "websocket.hpp" #include #include #include #include #include #include #include #include #include #include #include #include namespace crow { namespace obmc_vm { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static crow::websocket::Connection* session = nullptr; // The max network block device buffer size is 128kb plus 16bytes // for the message header: // https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md#simple-reply-message static constexpr auto nbdBufferSize = (128 * 1024 + 16) * 4; class Handler : public std::enable_shared_from_this { public: Handler(const std::string& media, boost::asio::io_context& ios) : pipeOut(ios), pipeIn(ios), proxy(ios, "/usr/bin/nbd-proxy", {media}, boost::process::v2::process_stdio{ .in = pipeIn, .out = pipeOut, .err = nullptr}), outputBuffer(new boost::beast::flat_static_buffer), inputBuffer(new boost::beast::flat_static_buffer) {} ~Handler() = default; Handler(const Handler&) = delete; Handler(Handler&&) = delete; Handler& operator=(const Handler&) = delete; Handler& operator=(Handler&&) = delete; void doClose() { // boost::process::child::terminate uses SIGKILL, need to send SIGTERM // to allow the proxy to stop nbd-client and the USB device gadget. int rc = kill(proxy.id(), SIGTERM); if (rc != 0) { BMCWEB_LOG_ERROR("Failed to terminate nbd-proxy: {}", errno); return; } proxy.wait(); } void connect() { std::error_code ec; if (ec) { BMCWEB_LOG_ERROR("Couldn't connect to nbd-proxy: {}", ec.message()); if (session != nullptr) { session->close("Error connecting to nbd-proxy"); } return; } doWrite(); doRead(); } void doWrite() { if (doingWrite) { BMCWEB_LOG_DEBUG("Already writing. Bailing out"); return; } if (inputBuffer->size() == 0) { BMCWEB_LOG_DEBUG("inputBuffer empty. Bailing out"); return; } doingWrite = true; pipeIn.async_write_some( inputBuffer->data(), [this, self(shared_from_this())](const boost::beast::error_code& ec, std::size_t bytesWritten) { BMCWEB_LOG_DEBUG("Wrote {}bytes", bytesWritten); doingWrite = false; inputBuffer->consume(bytesWritten); if (session == nullptr) { return; } if (ec == boost::asio::error::eof) { session->close("VM socket port closed"); return; } if (ec) { session->close("Error in writing to proxy port"); BMCWEB_LOG_ERROR("Error in VM socket write {}", ec); return; } doWrite(); }); } void doRead() { std::size_t bytes = outputBuffer->capacity() - outputBuffer->size(); pipeOut.async_read_some( outputBuffer->prepare(bytes), [this, self(shared_from_this())]( const boost::system::error_code& ec, std::size_t bytesRead) { BMCWEB_LOG_DEBUG("Read done. Read {} bytes", bytesRead); if (ec) { BMCWEB_LOG_ERROR("Couldn't read from VM port: {}", ec); if (session != nullptr) { session->close("Error in connecting to VM port"); } return; } if (session == nullptr) { return; } outputBuffer->commit(bytesRead); std::string_view payload( static_cast(outputBuffer->data().data()), bytesRead); session->sendBinary(payload); outputBuffer->consume(bytesRead); doRead(); }); } boost::asio::readable_pipe pipeOut; boost::asio::writable_pipe pipeIn; boost::process::v2::process proxy; bool doingWrite{false}; std::unique_ptr> outputBuffer; std::unique_ptr> inputBuffer; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static std::shared_ptr handler; } // namespace obmc_vm namespace nbd_proxy { using boost::asio::local::stream_protocol; // The max network block device buffer size is 128kb plus 16bytes // for the message header: // https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md#simple-reply-message static constexpr auto nbdBufferSize = (128 * 1024 + 16) * 4; struct NbdProxyServer : std::enable_shared_from_this { NbdProxyServer(crow::websocket::Connection& connIn, const std::string& socketIdIn, const std::string& endpointIdIn, const std::string& pathIn) : socketId(socketIdIn), endpointId(endpointIdIn), path(pathIn), peerSocket(connIn.getIoContext()), acceptor(connIn.getIoContext(), stream_protocol::endpoint(socketId)), connection(connIn) {} NbdProxyServer(const NbdProxyServer&) = delete; NbdProxyServer(NbdProxyServer&&) = delete; NbdProxyServer& operator=(const NbdProxyServer&) = delete; NbdProxyServer& operator=(NbdProxyServer&&) = delete; ~NbdProxyServer() { BMCWEB_LOG_DEBUG("NbdProxyServer destructor"); BMCWEB_LOG_DEBUG("peerSocket->close()"); boost::system::error_code ec; peerSocket.close(ec); BMCWEB_LOG_DEBUG("std::filesystem::remove({})", socketId); std::error_code ec2; std::filesystem::remove(socketId.c_str(), ec2); if (ec2) { BMCWEB_LOG_DEBUG("Failed to remove file, ignoring"); } crow::connections::systemBus->async_method_call( dbus::utility::logError, "xyz.openbmc_project.VirtualMedia", path, "xyz.openbmc_project.VirtualMedia.Proxy", "Unmount"); } std::string getEndpointId() const { return endpointId; } static void afterMount(const std::weak_ptr& weak, const boost::system::error_code& ec, bool /*isBinary*/) { std::shared_ptr self = weak.lock(); if (self == nullptr) { return; } if (ec) { BMCWEB_LOG_ERROR("DBus error: cannot call mount method = {}", ec.message()); self->connection.close("Failed to mount media"); return; } } static void afterAccept(const std::weak_ptr& weak, const boost::system::error_code& ec, stream_protocol::socket socket) { if (ec) { BMCWEB_LOG_ERROR("UNIX socket: async_accept error = {}", ec.message()); return; } BMCWEB_LOG_DEBUG("Connection opened"); std::shared_ptr self = weak.lock(); if (self == nullptr) { return; } self->connection.resumeRead(); self->peerSocket = std::move(socket); // Start reading from socket self->doRead(); } void run() { acceptor.async_accept( std::bind_front(&NbdProxyServer::afterAccept, weak_from_this())); crow::connections::systemBus->async_method_call( [weak{weak_from_this()}](const boost::system::error_code& ec, bool isBinary) { afterMount(weak, ec, isBinary); }, "xyz.openbmc_project.VirtualMedia", path, "xyz.openbmc_project.VirtualMedia.Proxy", "Mount"); } void send(std::string_view buffer, std::function&& onDone) { size_t copied = boost::asio::buffer_copy( ws2uxBuf.prepare(buffer.size()), boost::asio::buffer(buffer)); ws2uxBuf.commit(copied); doWrite(std::move(onDone)); } private: static void afterSendEx(const std::weak_ptr& weak) { std::shared_ptr self2 = weak.lock(); if (self2 != nullptr) { self2->ux2wsBuf.consume(self2->ux2wsBuf.size()); self2->doRead(); } } void afterRead(const std::weak_ptr& weak, const boost::system::error_code& ec, size_t bytesRead) { if (ec) { BMCWEB_LOG_ERROR("UNIX socket: async_read_some error = {}", ec.message()); return; } std::shared_ptr self = weak.lock(); if (self == nullptr) { return; } // Send to websocket self->ux2wsBuf.commit(bytesRead); self->connection.sendEx( crow::websocket::MessageType::Binary, boost::beast::buffers_to_string(self->ux2wsBuf.data()), std::bind_front(&NbdProxyServer::afterSendEx, weak_from_this())); } void doRead() { // Trigger async read peerSocket.async_read_some(ux2wsBuf.prepare(nbdBufferSize), std::bind_front(&NbdProxyServer::afterRead, this, weak_from_this())); } static void afterWrite(const std::weak_ptr& weak, std::function&& onDone, const boost::system::error_code& ec, size_t bytesWritten) { std::shared_ptr self = weak.lock(); if (self == nullptr) { return; } self->ws2uxBuf.consume(bytesWritten); self->uxWriteInProgress = false; if (ec) { BMCWEB_LOG_ERROR("UNIX: async_write error = {}", ec.message()); self->connection.close("Internal error"); return; } // Retrigger doWrite if there is something in buffer if (self->ws2uxBuf.size() > 0) { self->doWrite(std::move(onDone)); return; } onDone(); } void doWrite(std::function&& onDone) { if (uxWriteInProgress) { BMCWEB_LOG_ERROR("Write in progress"); return; } if (ws2uxBuf.size() == 0) { BMCWEB_LOG_ERROR("No data to write to UNIX socket"); return; } uxWriteInProgress = true; peerSocket.async_write_some( ws2uxBuf.data(), std::bind_front(&NbdProxyServer::afterWrite, weak_from_this(), std::move(onDone))); } // Keeps UNIX socket endpoint file path const std::string socketId; const std::string endpointId; const std::string path; bool uxWriteInProgress = false; // UNIX => WebSocket buffer boost::beast::flat_static_buffer ux2wsBuf; // WebSocket => UNIX buffer boost::beast::flat_static_buffer ws2uxBuf; // The socket used to communicate with the client. stream_protocol::socket peerSocket; // Default acceptor for UNIX socket stream_protocol::acceptor acceptor; crow::websocket::Connection& connection; }; using SessionMap = boost::container::flat_map>; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static SessionMap sessions; inline void afterGetSocket(crow::websocket::Connection& conn, const sdbusplus::message::object_path& path, const boost::system::error_code& ec, const dbus::utility::DBusPropertiesMap& propertiesList) { if (ec) { BMCWEB_LOG_ERROR("DBus getAllProperties error: {}", ec.message()); conn.close("Internal Error"); return; } std::string endpointId; std::string socket; bool success = sdbusplus::unpackPropertiesNoThrow( redfish::dbus_utils::UnpackErrorPrinter(), propertiesList, "EndpointId", endpointId, "Socket", socket); if (!success) { BMCWEB_LOG_ERROR("Failed to unpack properties"); conn.close("Internal Error"); return; } for (const auto& session : sessions) { if (session.second->getEndpointId() == conn.url().path()) { BMCWEB_LOG_ERROR("Cannot open new connection - socket is in use"); conn.close("Slot is in use"); return; } } // If the socket file exists (i.e. after bmcweb crash), // we cannot reuse it. std::error_code ec2; std::filesystem::remove(socket.c_str(), ec2); // Ignore failures. File might not exist. sessions[&conn] = std::make_shared(conn, socket, endpointId, path); sessions[&conn]->run(); } inline void onOpen(crow::websocket::Connection& conn) { BMCWEB_LOG_DEBUG("nbd-proxy.onopen({})", logPtr(&conn)); if (conn.url().segments().size() < 2) { BMCWEB_LOG_ERROR("Invalid path - \"{}\"", conn.url().path()); conn.close("Internal error"); return; } std::string index = conn.url().segments().back(); std::string path = std::format("/xyz/openbmc_project/VirtualMedia/Proxy/Slot_{}", index); dbus::utility::getAllProperties( "xyz.openbmc_project.VirtualMedia", path, "xyz.openbmc_project.VirtualMedia.MountPoint", [&conn, path](const boost::system::error_code& ec, const dbus::utility::DBusPropertiesMap& propertiesList) { afterGetSocket(conn, path, ec, propertiesList); }); // We need to wait for dbus and the websockets to hook up before data is // sent/received. Tell the core to hold off messages until the sockets are // up conn.deferRead(); } inline void onClose(crow::websocket::Connection& conn, const std::string& reason) { BMCWEB_LOG_DEBUG("nbd-proxy.onclose(reason = '{}')", reason); auto session = sessions.find(&conn); if (session == sessions.end()) { BMCWEB_LOG_DEBUG("No session to close"); return; } // Remove reference to session in global map sessions.erase(session); } inline void onMessage(crow::websocket::Connection& conn, std::string_view data, crow::websocket::MessageType /*type*/, std::function&& whenComplete) { BMCWEB_LOG_DEBUG("nbd-proxy.onMessage(len = {})", data.size()); // Acquire proxy from sessions auto session = sessions.find(&conn); if (session == sessions.end() || session->second == nullptr) { whenComplete(); return; } session->second->send(data, std::move(whenComplete)); } } // namespace nbd_proxy namespace obmc_vm { inline void requestRoutes(App& app) { static_assert( !(BMCWEB_VM_WEBSOCKET && BMCWEB_VM_NBDPROXY), "nbd proxy cannot be turned on at the same time as vm websocket."); if constexpr (BMCWEB_VM_NBDPROXY) { BMCWEB_ROUTE(app, "/nbd/") .privileges({{"ConfigureComponents", "ConfigureManager"}}) .websocket() .onopen(nbd_proxy::onOpen) .onclose(nbd_proxy::onClose) .onmessageex(nbd_proxy::onMessage); BMCWEB_ROUTE(app, "/vm/0/0") .privileges({{"ConfigureComponents", "ConfigureManager"}}) .websocket() .onopen(nbd_proxy::onOpen) .onclose(nbd_proxy::onClose) .onmessageex(nbd_proxy::onMessage); } if constexpr (BMCWEB_VM_WEBSOCKET) { BMCWEB_ROUTE(app, "/vm/0/0") .privileges({{"ConfigureComponents", "ConfigureManager"}}) .websocket() .onopen([](crow::websocket::Connection& conn) { BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn)); if (session != nullptr) { conn.close("Session already connected"); return; } if (handler != nullptr) { conn.close("Handler already running"); return; } session = &conn; // media is the last digit of the endpoint /vm/0/0. A future // enhancement can include supporting different endpoint values. const char* media = "0"; handler = std::make_shared(media, conn.getIoContext()); handler->connect(); }) .onclose([](crow::websocket::Connection& conn, const std::string& /*reason*/) { if (&conn != session) { return; } session = nullptr; handler->doClose(); handler->inputBuffer->clear(); handler->outputBuffer->clear(); handler.reset(); }) .onmessage([](crow::websocket::Connection& conn, const std::string& data, bool) { if (data.length() > handler->inputBuffer->capacity() - handler->inputBuffer->size()) { BMCWEB_LOG_ERROR("Buffer overrun when writing {} bytes", data.length()); conn.close("Buffer overrun"); return; } size_t copied = boost::asio::buffer_copy( handler->inputBuffer->prepare(data.size()), boost::asio::buffer(data)); handler->inputBuffer->commit(copied); handler->doWrite(); }); } } } // namespace obmc_vm } // namespace crow