diff options
Diffstat (limited to 'include/nbd_proxy.hpp')
| -rw-r--r-- | include/nbd_proxy.hpp | 381 |
1 files changed, 381 insertions, 0 deletions
diff --git a/include/nbd_proxy.hpp b/include/nbd_proxy.hpp new file mode 100644 index 0000000..64578f2 --- /dev/null +++ b/include/nbd_proxy.hpp @@ -0,0 +1,381 @@ +/* +// 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.h> +#include <websocket.h> + +#include <boost/asio.hpp> +#include <boost/beast/core/buffers_to_string.hpp> +#include <boost/beast/core/multi_buffer.hpp> +#include <boost/container/flat_map.hpp> +#include <dbus_utility.hpp> +#include <experimental/filesystem> +#include <variant> +#include <webserver_common.hpp> + +namespace crow +{ + +namespace nbd_proxy +{ + +using boost::asio::local::stream_protocol; + +static constexpr auto nbdBufferSize = 131088; + +struct NbdProxyServer : std::enable_shared_from_this<NbdProxyServer> +{ + NbdProxyServer(crow::websocket::Connection& connIn, + const std::string& socketIdIn, + const std::string& endpointIdIn, const std::string& pathIn) : + socketId(socketIdIn), + endpointId(endpointIdIn), path(pathIn), + acceptor(connIn.get_io_context(), stream_protocol::endpoint(socketId)), + connection(connIn) + { + } + + ~NbdProxyServer() + { + BMCWEB_LOG_DEBUG << "NbdProxyServer destructor"; + close(); + connection.close(); + + if (peerSocket) + { + BMCWEB_LOG_DEBUG << "peerSocket->close()"; + peerSocket->close(); + peerSocket.reset(); + BMCWEB_LOG_DEBUG << "std::remove(" << socketId << ")"; + std::remove(socketId.c_str()); + } + } + + std::string getEndpointId() const + { + return endpointId; + } + + void run() + { + acceptor.async_accept( + [this, self(shared_from_this())](boost::system::error_code ec, + stream_protocol::socket socket) { + if (ec) + { + BMCWEB_LOG_ERROR << "Cannot accept new connection: " << ec; + return; + } + if (peerSocket) + { + // Something is wrong - socket shouldn't be acquired at this + // point + BMCWEB_LOG_ERROR + << "Failed to open connection - socket already used"; + return; + } + + BMCWEB_LOG_DEBUG << "Connection opened"; + peerSocket = std::move(socket); + doRead(); + + // Trigger Write if any data was sent from server + // Initially this is negotiation chunk + doWrite(); + }); + + auto mountHandler = [](const boost::system::error_code ec, + const bool status) { + if (ec) + { + BMCWEB_LOG_ERROR << "DBus error: " << ec + << ", cannot call mount method"; + return; + } + }; + + crow::connections::systemBus->async_method_call( + std::move(mountHandler), "xyz.openbmc_project.VirtualMedia", path, + "xyz.openbmc_project.VirtualMedia.Proxy", "Mount"); + } + + void send(const std::string_view data) + { + boost::asio::buffer_copy(ws2uxBuf.prepare(data.size()), + boost::asio::buffer(data)); + ws2uxBuf.commit(data.size()); + doWrite(); + } + + void close() + { + // The reference to session should exists until unmount is + // called + auto unmountHandler = [](const boost::system::error_code ec) { + if (ec) + { + BMCWEB_LOG_ERROR << "DBus error: " << ec + << ", cannot call unmount method"; + return; + } + }; + + crow::connections::systemBus->async_method_call( + std::move(unmountHandler), "xyz.openbmc_project.VirtualMedia", path, + "xyz.openbmc_project.VirtualMedia.Proxy", "Unmount"); + } + + private: + void doRead() + { + if (!peerSocket) + { + BMCWEB_LOG_DEBUG << "UNIX socket isn't created yet"; + // Skip if UNIX socket is not created yet. + return; + } + + // Trigger async read + peerSocket->async_read_some( + ux2wsBuf.prepare(nbdBufferSize), + [this, self(shared_from_this())](boost::system::error_code ec, + std::size_t bytesRead) { + if (ec) + { + BMCWEB_LOG_ERROR << "UNIX socket: async_read_some error = " + << ec; + // UNIX socket has been closed by peer, best we can do is to + // break all connections + close(); + return; + } + + // Fetch data from UNIX socket + + ux2wsBuf.commit(bytesRead); + + // Paste it to WebSocket as binary + connection.sendBinary( + boost::beast::buffers_to_string(ux2wsBuf.data())); + ux2wsBuf.consume(bytesRead); + + // Allow further reads + doRead(); + }); + } + + void doWrite() + { + if (!peerSocket) + { + BMCWEB_LOG_DEBUG << "UNIX socket isn't created yet"; + // Skip if UNIX socket is not created yet. Collect data, and wait + // for nbd-client connection + return; + } + + 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; + boost::asio::async_write( + *peerSocket, ws2uxBuf.data(), + [this, self(shared_from_this())](boost::system::error_code ec, + std::size_t bytesWritten) { + ws2uxBuf.consume(bytesWritten); + uxWriteInProgress = false; + if (ec) + { + BMCWEB_LOG_ERROR << "UNIX: async_write error = " << ec; + return; + } + // Retrigger doWrite if there is something in buffer + doWrite(); + }); + } + + // 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::multi_buffer ux2wsBuf; + + // WebSocket <= UNIX buffer + boost::beast::multi_buffer ws2uxBuf; + + // Default acceptor for UNIX socket + stream_protocol::acceptor acceptor; + + // The socket used to communicate with the client. + std::optional<stream_protocol::socket> peerSocket; + + crow::websocket::Connection& connection; +}; + +static boost::container::flat_map<crow::websocket::Connection*, + std::shared_ptr<NbdProxyServer>> + sessions; + +void requestRoutes(CrowApp& app) +{ + BMCWEB_ROUTE(app, "/nbd/<str>") + .websocket() + .onopen([&app](crow::websocket::Connection& conn, + std::shared_ptr<bmcweb::AsyncResp> asyncResp) { + BMCWEB_LOG_DEBUG << "nbd-proxy.onopen(" << &conn << ")"; + + for (const auto session : sessions) + { + if (session.second->getEndpointId() == conn.req.target()) + { + BMCWEB_LOG_ERROR + << "Cannot open new connection - socket is in use"; + return; + } + } + + auto openHandler = [asyncResp, &conn]( + const boost::system::error_code ec, + 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; + return; + } + + for (const auto& objectPath : objects) + { + const auto interfaceMap = objectPath.second.find( + "xyz.openbmc_project.VirtualMedia.MountPoint"); + + if (interfaceMap == objectPath.second.end()) + { + BMCWEB_LOG_DEBUG << "Cannot find MountPoint object"; + continue; + } + + const auto endpoint = + interfaceMap->second.find("EndpointId"); + if (endpoint == interfaceMap->second.end()) + { + BMCWEB_LOG_DEBUG << "Cannot find EndpointId property"; + continue; + } + + endpointValue = std::get_if<std::string>(&endpoint->second); + + if (endpointValue == nullptr) + { + BMCWEB_LOG_ERROR << "EndpointId property value is null"; + continue; + } + + if (*endpointValue == conn.req.target()) + { + const auto socket = interfaceMap->second.find("Socket"); + if (socket == interfaceMap->second.end()) + { + BMCWEB_LOG_DEBUG << "Cannot find Socket property"; + continue; + } + + socketValue = std::get_if<std::string>(&socket->second); + if (socketValue == nullptr) + { + BMCWEB_LOG_ERROR << "Socket property value is null"; + continue; + } + + endpointObjectPath = &objectPath.first.str; + break; + } + } + + if (endpointObjectPath == nullptr) + { + BMCWEB_LOG_ERROR << "Cannot find requested EndpointId"; + asyncResp->res.result( + boost::beast::http::status::not_found); + 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<NbdProxyServer>( + conn, std::move(*socketValue), std::move(*endpointValue), + std::move(*endpointObjectPath)); + + sessions[&conn]->run(); + + asyncResp->res.result(boost::beast::http::status::ok); + }; + crow::connections::systemBus->async_method_call( + std::move(openHandler), "xyz.openbmc_project.VirtualMedia", + "/xyz/openbmc_project/VirtualMedia", + "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"); + }) + .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 + session->second->close(); + sessions.erase(session); + }) + .onmessage([](crow::websocket::Connection& conn, + const std::string& data, bool isBinary) { + BMCWEB_LOG_DEBUG << "nbd-proxy.onmessage(len = " << data.length() + << ")"; + // Acquire proxy from sessions + auto session = sessions.find(&conn); + if (session != sessions.end()) + { + if (session->second) + { + session->second->send(data); + return; + } + } + conn.close(); + }); +} +} // namespace nbd_proxy +} // namespace crow |

