/* // Copyright (c) 2019 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. */ #pragma once #include "app.hpp" #include "dbus_utility.hpp" #include "privileges.hpp" #include "websocket.hpp" #include #include #include #include #include namespace crow { namespace nbd_proxy { using boost::asio::local::stream_protocol; static constexpr size_t nbdBufferSize = 131088; constexpr const char* requiredPrivilegeString = "ConfigureManager"; 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; } void run() { acceptor.async_accept( [weak(weak_from_this())](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(); }); auto mountHandler = [weak(weak_from_this())]( const boost::system::error_code& ec, bool) { 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; } }; crow::connections::systemBus->async_method_call( std::move(mountHandler), "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: void doRead() { // Trigger async read peerSocket.async_read_some( ux2wsBuf.prepare(nbdBufferSize), [weak(weak_from_this())](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()), [weak(self->weak_from_this())]() { std::shared_ptr self2 = weak.lock(); if (self2 != nullptr) { self2->ux2wsBuf.consume(self2->ux2wsBuf.size()); self2->doRead(); } }); }); } 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(), [weak(weak_from_this()), onDone(std::move(onDone))](const boost::system::error_code& ec, size_t bytesWritten) mutable { 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(); }); } // 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 afterGetManagedObjects(crow::websocket::Connection& conn, const boost::system::error_code& ec, const dbus::utility::ManagedObjectType& objects) { const std::string* socketValue = nullptr; const std::string* endpointValue = nullptr; const std::string* endpointObjectPath = nullptr; if (ec) { BMCWEB_LOG_ERROR("DBus error: {}", ec.message()); conn.close("Failed to create mount point"); return; } for (const auto& [objectPath, interfaces] : objects) { for (const auto& [interface, properties] : interfaces) { if (interface != "xyz.openbmc_project.VirtualMedia.MountPoint") { continue; } for (const auto& [name, value] : properties) { if (name == "EndpointId") { endpointValue = std::get_if(&value); if (endpointValue == nullptr) { BMCWEB_LOG_ERROR("EndpointId property value is null"); } } if (name == "Socket") { socketValue = std::get_if(&value); if (socketValue == nullptr) { BMCWEB_LOG_ERROR("Socket property value is null"); } } } } if ((endpointValue != nullptr) && (socketValue != nullptr) && *endpointValue == conn.url().path()) { endpointObjectPath = &objectPath.str; break; } } if (objects.empty() || endpointObjectPath == nullptr) { BMCWEB_LOG_ERROR("Cannot find requested EndpointId"); conn.close("Failed to match EndpointId"); 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::remove((*socketValue).c_str()); sessions[&conn] = std::make_shared( conn, *socketValue, *endpointValue, *endpointObjectPath); sessions[&conn]->run(); }; inline void onOpen(crow::websocket::Connection& conn) { BMCWEB_LOG_DEBUG("nbd-proxy.onopen({})", logPtr(&conn)); sdbusplus::message::object_path path("/xyz/openbmc_project/VirtualMedia"); dbus::utility::getManagedObjects( "xyz.openbmc_project.VirtualMedia", path, [&conn](const boost::system::error_code& ec, const dbus::utility::ManagedObjectType& objects) { afterGetManagedObjects(conn, ec, objects); }); // 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)); } inline void requestRoutes(App& app) { BMCWEB_ROUTE(app, "/nbd/") .websocket() .onopen(onOpen) .onclose(onClose) .onmessageex(onMessage); } } // namespace nbd_proxy } // namespace crow