| /* |
| // 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 <boost/asio/buffer.hpp> |
| #include <boost/asio/local/stream_protocol.hpp> |
| #include <boost/asio/write.hpp> |
| #include <boost/beast/core/buffers_to_string.hpp> |
| #include <boost/beast/core/multi_buffer.hpp> |
| #include <boost/container/flat_map.hpp> |
| #include <websocket.hpp> |
| |
| namespace crow |
| { |
| |
| namespace nbd_proxy |
| { |
| |
| using boost::asio::local::stream_protocol; |
| |
| static constexpr auto nbdBufferSize = 131088; |
| constexpr const char* requiredPrivilegeString = "ConfigureManager"; |
| |
| 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.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"; |
| } |
| |
| std::string getEndpointId() const |
| { |
| return endpointId; |
| } |
| |
| void run() |
| { |
| acceptor.async_accept([this, self(shared_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; |
| } |
| 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 = |
| [this, self(shared_from_this())]( |
| const boost::system::error_code& ec, const bool) { |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "DBus error: cannot call mount method = " |
| << ec.message(); |
| 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 data) |
| { |
| boost::asio::buffer_copy(ws2uxBuf.prepare(data.size()), |
| boost::asio::buffer(data)); |
| ws2uxBuf.commit(data.size()); |
| doWrite(); |
| } |
| |
| void close() |
| { |
| acceptor.close(); |
| if (peerSocket) |
| { |
| BMCWEB_LOG_DEBUG << "peerSocket->close()"; |
| peerSocket->close(); |
| peerSocket.reset(); |
| BMCWEB_LOG_DEBUG << "std::remove(" << socketId << ")"; |
| std::remove(socketId.c_str()); |
| } |
| // 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())]( |
| const boost::system::error_code& ec, std::size_t bytesRead) { |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "UNIX socket: async_read_some error = " |
| << ec.message(); |
| // 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())]( |
| const boost::system::error_code& ec, std::size_t bytesWritten) { |
| ws2uxBuf.consume(bytesWritten); |
| uxWriteInProgress = false; |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "UNIX: async_write error = " |
| << ec.message(); |
| return; |
| } |
| // Retrigger doWrite if there is something in buffer |
| if (ws2uxBuf.size() > 0) |
| { |
| 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; |
| }; |
| |
| using SessionMap = boost::container::flat_map<crow::websocket::Connection*, |
| std::shared_ptr<NbdProxyServer>>; |
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) |
| static SessionMap sessions; |
| |
| inline void onOpen(crow::websocket::Connection& conn) |
| { |
| BMCWEB_LOG_DEBUG << "nbd-proxy.onopen(" << &conn << ")"; |
| |
| auto getUserInfoHandler = |
| [&conn](const boost::system::error_code& ec, |
| const dbus::utility::DBusPropertiesMap& userInfo) { |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "GetUserInfo failed..."; |
| conn.close("Failed to get user information"); |
| return; |
| } |
| |
| const std::string* userRolePtr = nullptr; |
| auto userInfoIter = |
| std::find_if(userInfo.begin(), userInfo.end(), [](const auto& p) { |
| return p.first == "UserPrivilege"; |
| }); |
| if (userInfoIter != userInfo.end()) |
| { |
| userRolePtr = std::get_if<std::string>(&userInfoIter->second); |
| } |
| |
| std::string userRole{}; |
| if (userRolePtr != nullptr) |
| { |
| userRole = *userRolePtr; |
| BMCWEB_LOG_DEBUG << "userName = " << conn.getUserName() |
| << " userRole = " << *userRolePtr; |
| } |
| |
| // Get the user privileges from the role |
| ::redfish::Privileges userPrivileges = |
| ::redfish::getUserPrivileges(userRole); |
| |
| const ::redfish::Privileges requiredPrivileges{requiredPrivilegeString}; |
| |
| if (!userPrivileges.isSupersetOf(requiredPrivileges)) |
| { |
| BMCWEB_LOG_DEBUG << "User " << conn.getUserName() |
| << " not authorized for nbd connection"; |
| conn.close("Unathourized access"); |
| return; |
| } |
| |
| auto openHandler = |
| [&conn](const boost::system::error_code& ec2, |
| const dbus::utility::ManagedObjectType& objects) { |
| const std::string* socketValue = nullptr; |
| const std::string* endpointValue = nullptr; |
| const std::string* endpointObjectPath = nullptr; |
| |
| if (ec2) |
| { |
| BMCWEB_LOG_ERROR << "DBus error: " << ec2.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<std::string>(&value); |
| |
| if (endpointValue == nullptr) |
| { |
| BMCWEB_LOG_ERROR |
| << "EndpointId property value is null"; |
| } |
| } |
| if (name == "Socket") |
| { |
| socketValue = std::get_if<std::string>(&value); |
| if (socketValue == nullptr) |
| { |
| BMCWEB_LOG_ERROR |
| << "Socket property value is null"; |
| } |
| } |
| } |
| } |
| |
| if ((endpointValue != nullptr) && (socketValue != nullptr) && |
| *endpointValue == conn.req.target()) |
| { |
| 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.req.target()) |
| { |
| 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<NbdProxyServer>( |
| conn, *socketValue, *endpointValue, *endpointObjectPath); |
| |
| sessions[&conn]->run(); |
| }; |
| crow::connections::systemBus->async_method_call( |
| std::move(openHandler), "xyz.openbmc_project.VirtualMedia", |
| "/xyz/openbmc_project/VirtualMedia", |
| "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"); |
| }; |
| |
| crow::connections::systemBus->async_method_call( |
| std::move(getUserInfoHandler), "xyz.openbmc_project.User.Manager", |
| "/xyz/openbmc_project/user", "xyz.openbmc_project.User.Manager", |
| "GetUserInfo", conn.getUserName()); |
| } |
| |
| 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; |
| } |
| session->second->close(); |
| // Remove reference to session in global map |
| sessions.erase(session); |
| } |
| |
| inline void 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; |
| } |
| } |
| } |
| |
| inline void requestRoutes(App& app) |
| { |
| BMCWEB_ROUTE(app, "/nbd/<str>") |
| .websocket() |
| .onopen(onOpen) |
| .onclose(onClose) |
| .onmessage(onMessage); |
| } |
| } // namespace nbd_proxy |
| } // namespace crow |