/* // 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 #include #include #include #include #include #include #include #include #include #include namespace crow { namespace nbd_proxy { using boost::asio::local::stream_protocol; static constexpr auto nbdBufferSize = 131088; static 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), acceptor(connIn.getIoContext(), stream_protocol::endpoint(socketId)), connection(connIn) {} ~NbdProxyServer() { BMCWEB_LOG_DEBUG << "NbdProxyServer destructor"; } 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 << "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(const 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())](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())](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 peerSocket; crow::websocket::Connection& connection; }; static boost::container::flat_map> sessions; inline void requestRoutes(App& app) { BMCWEB_ROUTE(app, "/nbd/") .websocket() .onopen([](crow::websocket::Connection& conn, const std::shared_ptr& asyncResp) { BMCWEB_LOG_DEBUG << "nbd-proxy.onopen(" << &conn << ")"; auto getUserInfoHandler = [&conn, asyncResp]( const boost::system::error_code ec, boost::container::flat_map< std::string, std::variant>> userInfo) { if (ec) { BMCWEB_LOG_ERROR << "GetUserInfo failed..."; conn.close("Failed to get user information"); return; } const std::string* userRolePtr = nullptr; auto userInfoIter = userInfo.find("UserPrivilege"); if (userInfoIter != userInfo.end()) { userRolePtr = std::get_if(&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, asyncResp]( 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 : 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(&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(&socket->second); if (socketValue == nullptr) { BMCWEB_LOG_ERROR << "Socket property value is null"; continue; } endpointObjectPath = &objectPath.first.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( 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()); }) .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); session->second->close(); }) .onmessage([](crow::websocket::Connection& conn, const std::string& data, bool) { 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; } } }); } } // namespace nbd_proxy } // namespace crow