diff --git a/CMakeLists.txt b/CMakeLists.txt index dfac273024..9303e4dae0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,11 @@ option ( See https://github.com/openbmc/jsnbd/blob/master/README." ON ) +option ( + BMCWEB_ENABLE_VM_NBDPROXY + "Enable the Virtual Media WebSocket." + OFF +) option ( BMCWEB_ENABLE_DBUS_REST "Enable Phosphor REST (D-Bus) APIs. Paths directly map Phosphor D-Bus @@ -360,6 +365,7 @@ target_compile_definitions ( bmcweb PRIVATE $<$: -DBMCWEB_ENABLE_KVM> $<$: -DBMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION> $<$: -DBMCWEB_ENABLE_VM_WEBSOCKET> + $<$: -DBMCWEB_ENABLE_VM_NBDPROXY> $<$: -DBMCWEB_ENABLE_DBUS_REST> $<$: -DBMCWEB_ENABLE_REDFISH> $<$: -DBMCWEB_ENABLE_STATIC_HOSTING> diff --git a/http/routing.h b/http/routing.h index 200cfa09fb..7846924b8b 100644 --- a/http/routing.h +++ b/http/routing.h @@ -3,6 +3,7 @@ #include "privileges.hpp" #include "sessions.hpp" +#include #include #include #include @@ -323,19 +324,19 @@ class WebSocketRule : public BaseRule res.end(); } - void handleUpgrade(const Request& req, Response&, + void handleUpgrade(const Request& req, Response& res, boost::asio::ip::tcp::socket&& adaptor) override { std::shared_ptr< crow::websocket::ConnectionImpl> myConnection = std::make_shared< crow::websocket::ConnectionImpl>( - req, std::move(adaptor), openHandler, messageHandler, + req, res, std::move(adaptor), openHandler, messageHandler, closeHandler, errorHandler); myConnection->start(); } #ifdef BMCWEB_ENABLE_SSL - void handleUpgrade(const Request& req, Response&, + void handleUpgrade(const Request& req, Response& res, boost::beast::ssl_stream&& adaptor) override { @@ -343,7 +344,7 @@ class WebSocketRule : public BaseRule boost::beast::ssl_stream>> myConnection = std::make_shared>>( - req, std::move(adaptor), openHandler, messageHandler, + req, res, std::move(adaptor), openHandler, messageHandler, closeHandler, errorHandler); myConnection->start(); } @@ -374,7 +375,9 @@ class WebSocketRule : public BaseRule } protected: - std::function openHandler; + std::function)> + openHandler; std::function messageHandler; std::function diff --git a/http/websocket.h b/http/websocket.h index 61b3e5aa50..80d536a19d 100644 --- a/http/websocket.h +++ b/http/websocket.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -15,10 +16,11 @@ namespace crow { namespace websocket { + struct Connection : std::enable_shared_from_this { public: - explicit Connection(const crow::Request& reqIn) : + explicit Connection(const crow::Request& reqIn, crow::Response& res) : req(reqIn), userdataPtr(nullptr){}; virtual void sendBinary(const std::string_view msg) = 0; @@ -39,6 +41,7 @@ struct Connection : std::enable_shared_from_this } crow::Request req; + crow::Response res; private: void* userdataPtr; @@ -48,13 +51,14 @@ template class ConnectionImpl : public Connection { public: ConnectionImpl( - const crow::Request& reqIn, Adaptor adaptorIn, - std::function open_handler, + const crow::Request& reqIn, crow::Response& res, Adaptor adaptorIn, + std::function)> + open_handler, std::function message_handler, std::function close_handler, std::function error_handler) : - Connection(reqIn), + Connection(reqIn, res), ws(std::move(adaptorIn)), inString(), inBuffer(inString, 131088), openHandler(std::move(open_handler)), messageHandler(std::move(message_handler)), @@ -158,11 +162,15 @@ template class ConnectionImpl : public Connection { BMCWEB_LOG_DEBUG << "Websocket accepted connection"; + auto asyncResp = std::make_shared( + res, [this, self(shared_from_this())]() { doRead(); }); + + asyncResp->res.result(boost::beast::http::status::ok); + if (openHandler) { - openHandler(*this); + openHandler(*this, asyncResp); } - doRead(); } void doRead() @@ -241,7 +249,8 @@ template class ConnectionImpl : public Connection std::vector outBuffer; bool doingWrite = false; - std::function openHandler; + std::function)> + openHandler; std::function messageHandler; std::function closeHandler; std::function errorHandler; diff --git a/include/async_resp.hpp b/include/async_resp.hpp index af4edebc53..78994cf77e 100644 --- a/include/async_resp.hpp +++ b/include/async_resp.hpp @@ -1,5 +1,7 @@ #pragma once +#include + namespace bmcweb { @@ -7,6 +9,7 @@ namespace bmcweb * AsyncResp * Gathers data needed for response processing after async calls are done */ + class AsyncResp { public: @@ -14,12 +17,23 @@ class AsyncResp { } + AsyncResp(crow::Response& response, std::function&& function) : + res(response), func(std::move(function)) + { + } + ~AsyncResp() { + if (func && res.result() == boost::beast::http::status::ok) + { + func(); + } + res.end(); } crow::Response& res; + std::function func = 0; }; -} // namespace bmcweb \ No newline at end of file +} // namespace bmcweb diff --git a/include/dbus_monitor.hpp b/include/dbus_monitor.hpp index 0543c7b9f5..1747810c68 100644 --- a/include/dbus_monitor.hpp +++ b/include/dbus_monitor.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -116,7 +117,8 @@ template void requestRoutes(Crow& app) BMCWEB_ROUTE(app, "/subscribe") .requires({"Login"}) .websocket() - .onopen([&](crow::websocket::Connection& conn) { + .onopen([&](crow::websocket::Connection& conn, + std::shared_ptr asyncResp) { BMCWEB_LOG_DEBUG << "Connection " << &conn << " opened"; sessions[&conn] = DbusWebsocketSession(); }) diff --git a/include/kvm_websocket.hpp b/include/kvm_websocket.hpp index d97b03e885..306c68409d 100644 --- a/include/kvm_websocket.hpp +++ b/include/kvm_websocket.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -161,7 +162,8 @@ inline void requestRoutes(CrowApp& app) BMCWEB_ROUTE(app, "/kvm/0") .requires({"ConfigureComponents", "ConfigureManager"}) .websocket() - .onopen([](crow::websocket::Connection& conn) { + .onopen([](crow::websocket::Connection& conn, + std::shared_ptr asyncResp) { BMCWEB_LOG_DEBUG << "Connection " << &conn << " opened"; if (sessions.size() == maxSessions) diff --git a/include/nbd_proxy.hpp b/include/nbd_proxy.hpp new file mode 100644 index 0000000000..64578f2fe2 --- /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 +#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; + +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.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 peerSocket; + + crow::websocket::Connection& connection; +}; + +static boost::container::flat_map> + sessions; + +void requestRoutes(CrowApp& app) +{ + BMCWEB_ROUTE(app, "/nbd/") + .websocket() + .onopen([&app](crow::websocket::Connection& conn, + std::shared_ptr 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(&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 (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( + 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 diff --git a/include/obmc_console.hpp b/include/obmc_console.hpp index b545f960e1..9e5e058b85 100644 --- a/include/obmc_console.hpp +++ b/include/obmc_console.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -106,7 +107,8 @@ void requestRoutes(CrowApp& app) BMCWEB_ROUTE(app, "/console0") .requires({"ConfigureComponents", "ConfigureManager"}) .websocket() - .onopen([](crow::websocket::Connection& conn) { + .onopen([](crow::websocket::Connection& conn, + std::shared_ptr asyncResp) { BMCWEB_LOG_DEBUG << "Connection " << &conn << " opened"; sessions.insert(&conn); diff --git a/include/vm_websocket.hpp b/include/vm_websocket.hpp index 92ccc0ddd9..a8380a735e 100644 --- a/include/vm_websocket.hpp +++ b/include/vm_websocket.hpp @@ -160,7 +160,8 @@ template void requestRoutes(Crow& app) BMCWEB_ROUTE(app, "/vm/0/0") .requires({"ConfigureComponents", "ConfigureManager"}) .websocket() - .onopen([](crow::websocket::Connection& conn) { + .onopen([](crow::websocket::Connection& conn, + std::shared_ptr asyncResp) { BMCWEB_LOG_DEBUG << "Connection " << &conn << " opened"; if (session != nullptr) diff --git a/src/webserver_main.cpp b/src/webserver_main.cpp index 78b4a41fa0..a2da120188 100644 --- a/src/webserver_main.cpp +++ b/src/webserver_main.cpp @@ -23,6 +23,10 @@ #include #include +#ifdef BMCWEB_ENABLE_VM_NBDPROXY +#include +#endif + constexpr int defaultPort = 18080; template @@ -97,6 +101,11 @@ int main(int argc, char** argv) crow::connections::systemBus = std::make_shared(*io); + +#ifdef BMCWEB_ENABLE_VM_NBDPROXY + crow::nbd_proxy::requestRoutes(app); +#endif + redfish::RedfishService redfish(app); // Keep the user role map hot in memory and