xref: /openbmc/bmcweb/include/vm_websocket.hpp (revision d98a2f9388b9cab1100d30de401da43c32c98ef4)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #pragma once
4 
5 #include "bmcweb_config.h"
6 
7 #include "app.hpp"
8 #include "dbus_singleton.hpp"
9 #include "dbus_utility.hpp"
10 #include "io_context_singleton.hpp"
11 #include "logging.hpp"
12 #include "websocket.hpp"
13 
14 #include <boost/asio/buffer.hpp>
15 #include <boost/asio/error.hpp>
16 #include <boost/asio/io_context.hpp>
17 #include <boost/asio/local/stream_protocol.hpp>
18 #include <boost/asio/readable_pipe.hpp>
19 #include <boost/asio/writable_pipe.hpp>
20 #include <boost/beast/core/buffers_to_string.hpp>
21 #include <boost/beast/core/error.hpp>
22 #include <boost/beast/core/flat_static_buffer.hpp>
23 #include <boost/container/flat_map.hpp>
24 #include <boost/process/v2/process.hpp>
25 #include <boost/process/v2/stdio.hpp>
26 #include <sdbusplus/message/native_types.hpp>
27 #include <sdbusplus/unpack_properties.hpp>
28 
29 #include <cerrno>
30 #include <csignal>
31 #include <cstddef>
32 #include <filesystem>
33 #include <format>
34 #include <functional>
35 #include <memory>
36 #include <string>
37 #include <string_view>
38 #include <system_error>
39 #include <utility>
40 
41 namespace crow
42 {
43 
44 namespace obmc_vm
45 {
46 
47 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
48 static crow::websocket::Connection* session = nullptr;
49 
50 // The max network block device buffer size is 128kb plus 16bytes
51 // for the message header:
52 // https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md#simple-reply-message
53 static constexpr auto nbdBufferSize = (128 * 1024 + 16) * 4;
54 
55 class Handler : public std::enable_shared_from_this<Handler>
56 {
57   public:
Handler(const std::string & media,boost::asio::io_context & ios)58     Handler(const std::string& media, boost::asio::io_context& ios) :
59         pipeOut(ios), pipeIn(ios),
60         proxy(ios, "/usr/bin/nbd-proxy", {media},
61               boost::process::v2::process_stdio{
62                   .in = pipeIn, .out = pipeOut, .err = nullptr})
63     {}
64 
65     ~Handler() = default;
66 
67     Handler(const Handler&) = delete;
68     Handler(Handler&&) = delete;
69     Handler& operator=(const Handler&) = delete;
70     Handler& operator=(Handler&&) = delete;
71 
doClose()72     void doClose()
73     {
74         // boost::process::child::terminate uses SIGKILL, need to send SIGTERM
75         // to allow the proxy to stop nbd-client and the USB device gadget.
76         // NOLINTNEXTLINE(misc-include-cleaner)
77         int rc = kill(proxy.id(), SIGTERM);
78         if (rc != 0)
79         {
80             BMCWEB_LOG_ERROR("Failed to terminate nbd-proxy: {}", errno);
81             return;
82         }
83 
84         proxy.wait();
85     }
86 
connect()87     void connect()
88     {
89         std::error_code ec;
90         if (ec)
91         {
92             BMCWEB_LOG_ERROR("Couldn't connect to nbd-proxy: {}", ec.message());
93             if (session != nullptr)
94             {
95                 session->close("Error connecting to nbd-proxy");
96             }
97             return;
98         }
99         doWrite();
100         doRead();
101     }
102 
doWrite()103     void doWrite()
104     {
105         if (doingWrite)
106         {
107             BMCWEB_LOG_DEBUG("Already writing.  Bailing out");
108             return;
109         }
110 
111         if (inputBuffer.size() == 0)
112         {
113             BMCWEB_LOG_DEBUG("inputBuffer empty.  Bailing out");
114             return;
115         }
116 
117         doingWrite = true;
118         pipeIn.async_write_some(
119             inputBuffer.data(),
120             [this, self(shared_from_this())](const boost::beast::error_code& ec,
121                                              std::size_t bytesWritten) {
122                 BMCWEB_LOG_DEBUG("Wrote {}bytes", bytesWritten);
123                 doingWrite = false;
124                 inputBuffer.consume(bytesWritten);
125 
126                 if (session == nullptr)
127                 {
128                     return;
129                 }
130                 if (ec == boost::asio::error::eof)
131                 {
132                     session->close("VM socket port closed");
133                     return;
134                 }
135                 if (ec)
136                 {
137                     session->close("Error in writing to proxy port");
138                     BMCWEB_LOG_ERROR("Error in VM socket write {}", ec);
139                     return;
140                 }
141                 doWrite();
142             });
143     }
144 
doRead()145     void doRead()
146     {
147         std::size_t bytes = outputBuffer.capacity() - outputBuffer.size();
148 
149         pipeOut.async_read_some(
150             outputBuffer.prepare(bytes),
151             [this, self(shared_from_this())](
152                 const boost::system::error_code& ec, std::size_t bytesRead) {
153                 BMCWEB_LOG_DEBUG("Read done.  Read {} bytes", bytesRead);
154                 if (ec)
155                 {
156                     BMCWEB_LOG_ERROR("Couldn't read from VM port: {}", ec);
157                     if (session != nullptr)
158                     {
159                         session->close("Error in connecting to VM port");
160                     }
161                     return;
162                 }
163                 if (session == nullptr)
164                 {
165                     return;
166                 }
167 
168                 outputBuffer.commit(bytesRead);
169                 std::string_view payload(
170                     static_cast<const char*>(outputBuffer.data().data()),
171                     bytesRead);
172                 session->sendBinary(payload);
173                 outputBuffer.consume(bytesRead);
174 
175                 doRead();
176             });
177     }
178 
179     boost::asio::readable_pipe pipeOut;
180     boost::asio::writable_pipe pipeIn;
181     boost::process::v2::process proxy;
182     bool doingWrite{false};
183 
184     boost::beast::flat_static_buffer<nbdBufferSize> outputBuffer;
185     boost::beast::flat_static_buffer<nbdBufferSize> inputBuffer;
186 };
187 
188 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
189 static std::shared_ptr<Handler> handler;
190 
191 } // namespace obmc_vm
192 
193 namespace nbd_proxy
194 {
195 using boost::asio::local::stream_protocol;
196 
197 // The max network block device buffer size is 128kb plus 16bytes
198 // for the message header:
199 // https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md#simple-reply-message
200 static constexpr auto nbdBufferSize = (128 * 1024 + 16) * 4;
201 
202 struct NbdProxyServer : std::enable_shared_from_this<NbdProxyServer>
203 {
NbdProxyServercrow::nbd_proxy::NbdProxyServer204     NbdProxyServer(crow::websocket::Connection& connIn,
205                    const std::string& socketIdIn,
206                    const std::string& endpointIdIn, const std::string& pathIn) :
207         socketId(socketIdIn), endpointId(endpointIdIn), path(pathIn),
208 
209         peerSocket(getIoContext()),
210         acceptor(getIoContext(), stream_protocol::endpoint(socketId)),
211         connection(connIn)
212     {}
213 
214     NbdProxyServer(const NbdProxyServer&) = delete;
215     NbdProxyServer(NbdProxyServer&&) = delete;
216     NbdProxyServer& operator=(const NbdProxyServer&) = delete;
217     NbdProxyServer& operator=(NbdProxyServer&&) = delete;
218 
~NbdProxyServercrow::nbd_proxy::NbdProxyServer219     ~NbdProxyServer()
220     {
221         BMCWEB_LOG_DEBUG("NbdProxyServer destructor");
222 
223         BMCWEB_LOG_DEBUG("peerSocket->close()");
224         boost::system::error_code ec;
225         peerSocket.close(ec);
226 
227         BMCWEB_LOG_DEBUG("std::filesystem::remove({})", socketId);
228         std::error_code ec2;
229         std::filesystem::remove(socketId.c_str(), ec2);
230         if (ec2)
231         {
232             BMCWEB_LOG_DEBUG("Failed to remove file, ignoring");
233         }
234 
235         crow::connections::systemBus->async_method_call(
236             dbus::utility::logError, "xyz.openbmc_project.VirtualMedia", path,
237             "xyz.openbmc_project.VirtualMedia.Proxy", "Unmount");
238     }
239 
getEndpointIdcrow::nbd_proxy::NbdProxyServer240     std::string getEndpointId() const
241     {
242         return endpointId;
243     }
244 
afterMountcrow::nbd_proxy::NbdProxyServer245     static void afterMount(const std::weak_ptr<NbdProxyServer>& weak,
246                            const boost::system::error_code& ec,
247                            bool /*isBinary*/)
248     {
249         std::shared_ptr<NbdProxyServer> self = weak.lock();
250         if (self == nullptr)
251         {
252             return;
253         }
254         if (ec)
255         {
256             BMCWEB_LOG_ERROR("DBus error: cannot call mount method = {}",
257                              ec.message());
258 
259             self->connection.close("Failed to mount media");
260             return;
261         }
262     }
263 
afterAcceptcrow::nbd_proxy::NbdProxyServer264     static void afterAccept(const std::weak_ptr<NbdProxyServer>& weak,
265                             const boost::system::error_code& ec,
266                             stream_protocol::socket socket)
267     {
268         if (ec)
269         {
270             BMCWEB_LOG_ERROR("UNIX socket: async_accept error = {}",
271                              ec.message());
272             return;
273         }
274 
275         BMCWEB_LOG_DEBUG("Connection opened");
276         std::shared_ptr<NbdProxyServer> self = weak.lock();
277         if (self == nullptr)
278         {
279             return;
280         }
281 
282         self->connection.resumeRead();
283         self->peerSocket = std::move(socket);
284         //  Start reading from socket
285         self->doRead();
286     }
287 
runcrow::nbd_proxy::NbdProxyServer288     void run()
289     {
290         acceptor.async_accept(
291             std::bind_front(&NbdProxyServer::afterAccept, weak_from_this()));
292 
293         crow::connections::systemBus->async_method_call(
294             [weak{weak_from_this()}](const boost::system::error_code& ec,
295                                      bool isBinary) {
296                 afterMount(weak, ec, isBinary);
297             },
298             "xyz.openbmc_project.VirtualMedia", path,
299             "xyz.openbmc_project.VirtualMedia.Proxy", "Mount");
300     }
301 
sendcrow::nbd_proxy::NbdProxyServer302     void send(std::string_view buffer, std::function<void()>&& onDone)
303     {
304         size_t copied = boost::asio::buffer_copy(
305             ws2uxBuf.prepare(buffer.size()), boost::asio::buffer(buffer));
306         ws2uxBuf.commit(copied);
307 
308         doWrite(std::move(onDone));
309     }
310 
311   private:
afterSendExcrow::nbd_proxy::NbdProxyServer312     static void afterSendEx(const std::weak_ptr<NbdProxyServer>& weak)
313     {
314         std::shared_ptr<NbdProxyServer> self2 = weak.lock();
315         if (self2 != nullptr)
316         {
317             self2->ux2wsBuf.consume(self2->ux2wsBuf.size());
318             self2->doRead();
319         }
320     }
321 
afterReadcrow::nbd_proxy::NbdProxyServer322     void afterRead(const std::weak_ptr<NbdProxyServer>& weak,
323                    const boost::system::error_code& ec, size_t bytesRead)
324     {
325         if (ec)
326         {
327             BMCWEB_LOG_ERROR("UNIX socket: async_read_some error = {}",
328                              ec.message());
329             return;
330         }
331         std::shared_ptr<NbdProxyServer> self = weak.lock();
332         if (self == nullptr)
333         {
334             return;
335         }
336 
337         // Send to websocket
338         self->ux2wsBuf.commit(bytesRead);
339         self->connection.sendEx(
340             crow::websocket::MessageType::Binary,
341             boost::beast::buffers_to_string(self->ux2wsBuf.data()),
342             std::bind_front(&NbdProxyServer::afterSendEx, weak_from_this()));
343     }
344 
doReadcrow::nbd_proxy::NbdProxyServer345     void doRead()
346     {
347         // Trigger async read
348         peerSocket.async_read_some(ux2wsBuf.prepare(nbdBufferSize),
349                                    std::bind_front(&NbdProxyServer::afterRead,
350                                                    this, weak_from_this()));
351     }
352 
afterWritecrow::nbd_proxy::NbdProxyServer353     static void afterWrite(const std::weak_ptr<NbdProxyServer>& weak,
354                            std::function<void()>&& onDone,
355                            const boost::system::error_code& ec,
356                            size_t bytesWritten)
357     {
358         std::shared_ptr<NbdProxyServer> self = weak.lock();
359         if (self == nullptr)
360         {
361             return;
362         }
363 
364         self->ws2uxBuf.consume(bytesWritten);
365         self->uxWriteInProgress = false;
366 
367         if (ec)
368         {
369             BMCWEB_LOG_ERROR("UNIX: async_write error = {}", ec.message());
370             self->connection.close("Internal error");
371             return;
372         }
373 
374         // Retrigger doWrite if there is something in buffer
375         if (self->ws2uxBuf.size() > 0)
376         {
377             self->doWrite(std::move(onDone));
378             return;
379         }
380         onDone();
381     }
382 
doWritecrow::nbd_proxy::NbdProxyServer383     void doWrite(std::function<void()>&& onDone)
384     {
385         if (uxWriteInProgress)
386         {
387             BMCWEB_LOG_ERROR("Write in progress");
388             return;
389         }
390 
391         if (ws2uxBuf.size() == 0)
392         {
393             BMCWEB_LOG_ERROR("No data to write to UNIX socket");
394             return;
395         }
396 
397         uxWriteInProgress = true;
398         peerSocket.async_write_some(
399             ws2uxBuf.data(),
400             std::bind_front(&NbdProxyServer::afterWrite, weak_from_this(),
401                             std::move(onDone)));
402     }
403 
404     // Keeps UNIX socket endpoint file path
405     const std::string socketId;
406     const std::string endpointId;
407     const std::string path;
408 
409     bool uxWriteInProgress = false;
410 
411     // UNIX => WebSocket buffer
412     boost::beast::flat_static_buffer<nbdBufferSize> ux2wsBuf;
413 
414     // WebSocket => UNIX buffer
415     boost::beast::flat_static_buffer<nbdBufferSize> ws2uxBuf;
416 
417     // The socket used to communicate with the client.
418     stream_protocol::socket peerSocket;
419 
420     // Default acceptor for UNIX socket
421     stream_protocol::acceptor acceptor;
422 
423     crow::websocket::Connection& connection;
424 };
425 
426 using SessionMap = boost::container::flat_map<crow::websocket::Connection*,
427                                               std::shared_ptr<NbdProxyServer>>;
428 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
429 static SessionMap sessions;
430 
afterGetSocket(crow::websocket::Connection & conn,const sdbusplus::message::object_path & path,const boost::system::error_code & ec,const dbus::utility::DBusPropertiesMap & propertiesList)431 inline void afterGetSocket(
432     crow::websocket::Connection& conn,
433     const sdbusplus::message::object_path& path,
434     const boost::system::error_code& ec,
435     const dbus::utility::DBusPropertiesMap& propertiesList)
436 {
437     if (ec)
438     {
439         BMCWEB_LOG_ERROR("DBus getAllProperties error: {}", ec.message());
440         conn.close("Internal Error");
441         return;
442     }
443     std::string endpointId;
444     std::string socket;
445 
446     bool success = sdbusplus::unpackPropertiesNoThrow(
447         redfish::dbus_utils::UnpackErrorPrinter(), propertiesList, "EndpointId",
448         endpointId, "Socket", socket);
449 
450     if (!success)
451     {
452         BMCWEB_LOG_ERROR("Failed to unpack properties");
453         conn.close("Internal Error");
454         return;
455     }
456 
457     for (const auto& session : sessions)
458     {
459         if (session.second->getEndpointId() == conn.url().path())
460         {
461             BMCWEB_LOG_ERROR("Cannot open new connection - socket is in use");
462             conn.close("Slot is in use");
463             return;
464         }
465     }
466 
467     // If the socket file exists (i.e. after bmcweb crash),
468     // we cannot reuse it.
469     std::error_code ec2;
470     std::filesystem::remove(socket.c_str(), ec2);
471     // Ignore failures.  File might not exist.
472 
473     sessions[&conn] =
474         std::make_shared<NbdProxyServer>(conn, socket, endpointId, path);
475     sessions[&conn]->run();
476 }
477 
onOpen(crow::websocket::Connection & conn)478 inline void onOpen(crow::websocket::Connection& conn)
479 {
480     BMCWEB_LOG_DEBUG("nbd-proxy.onopen({})", logPtr(&conn));
481 
482     if (conn.url().segments().size() < 2)
483     {
484         BMCWEB_LOG_ERROR("Invalid path - \"{}\"", conn.url().path());
485         conn.close("Internal error");
486         return;
487     }
488 
489     std::string index = conn.url().segments().back();
490     std::string path =
491         std::format("/xyz/openbmc_project/VirtualMedia/Proxy/Slot_{}", index);
492 
493     dbus::utility::getAllProperties(
494         "xyz.openbmc_project.VirtualMedia", path,
495         "xyz.openbmc_project.VirtualMedia.MountPoint",
496         [&conn, path](const boost::system::error_code& ec,
497                       const dbus::utility::DBusPropertiesMap& propertiesList) {
498             afterGetSocket(conn, path, ec, propertiesList);
499         });
500 
501     // We need to wait for dbus and the websockets to hook up before data is
502     // sent/received.  Tell the core to hold off messages until the sockets are
503     // up
504     conn.deferRead();
505 }
506 
onClose(crow::websocket::Connection & conn,const std::string & reason)507 inline void onClose(crow::websocket::Connection& conn,
508                     const std::string& reason)
509 {
510     BMCWEB_LOG_DEBUG("nbd-proxy.onclose(reason = '{}')", reason);
511     auto session = sessions.find(&conn);
512     if (session == sessions.end())
513     {
514         BMCWEB_LOG_DEBUG("No session to close");
515         return;
516     }
517     // Remove reference to session in global map
518     sessions.erase(session);
519 }
520 
onMessage(crow::websocket::Connection & conn,std::string_view data,crow::websocket::MessageType,std::function<void ()> && whenComplete)521 inline void onMessage(crow::websocket::Connection& conn, std::string_view data,
522                       crow::websocket::MessageType /*type*/,
523                       std::function<void()>&& whenComplete)
524 {
525     BMCWEB_LOG_DEBUG("nbd-proxy.onMessage(len = {})", data.size());
526 
527     // Acquire proxy from sessions
528     auto session = sessions.find(&conn);
529     if (session == sessions.end() || session->second == nullptr)
530     {
531         whenComplete();
532         return;
533     }
534 
535     session->second->send(data, std::move(whenComplete));
536 }
537 } // namespace nbd_proxy
538 
539 namespace obmc_vm
540 {
541 
requestRoutes(App & app)542 inline void requestRoutes(App& app)
543 {
544     static_assert(
545         !(BMCWEB_VM_WEBSOCKET && BMCWEB_VM_NBDPROXY),
546         "nbd proxy cannot be turned on at the same time as vm websocket.");
547 
548     if constexpr (BMCWEB_VM_NBDPROXY)
549     {
550         BMCWEB_ROUTE(app, "/nbd/<str>")
551             .privileges({{"ConfigureComponents", "ConfigureManager"}})
552             .websocket()
553             .onopen(nbd_proxy::onOpen)
554             .onclose(nbd_proxy::onClose)
555             .onmessageex(nbd_proxy::onMessage);
556 
557         BMCWEB_ROUTE(app, "/vm/0/0")
558             .privileges({{"ConfigureComponents", "ConfigureManager"}})
559             .websocket()
560             .onopen(nbd_proxy::onOpen)
561             .onclose(nbd_proxy::onClose)
562             .onmessageex(nbd_proxy::onMessage);
563     }
564     if constexpr (BMCWEB_VM_WEBSOCKET)
565     {
566         BMCWEB_ROUTE(app, "/vm/0/0")
567             .privileges({{"ConfigureComponents", "ConfigureManager"}})
568             .websocket()
569             .onopen([](crow::websocket::Connection& conn) {
570                 BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn));
571 
572                 if (session != nullptr)
573                 {
574                     conn.close("Session already connected");
575                     return;
576                 }
577 
578                 if (handler != nullptr)
579                 {
580                     conn.close("Handler already running");
581                     return;
582                 }
583 
584                 session = &conn;
585 
586                 // media is the last digit of the endpoint /vm/0/0. A future
587                 // enhancement can include supporting different endpoint values.
588                 const char* media = "0";
589                 handler = std::make_shared<Handler>(media, getIoContext());
590                 handler->connect();
591             })
592             .onclose([](crow::websocket::Connection& conn,
593                         const std::string& /*reason*/) {
594                 if (&conn != session)
595                 {
596                     return;
597                 }
598 
599                 session = nullptr;
600                 handler->doClose();
601                 handler->inputBuffer.clear();
602                 handler->outputBuffer.clear();
603                 handler.reset();
604             })
605             .onmessage([](crow::websocket::Connection& conn,
606                           const std::string& data, bool) {
607                 if (data.length() > handler->inputBuffer.capacity() -
608                                         handler->inputBuffer.size())
609                 {
610                     BMCWEB_LOG_ERROR("Buffer overrun when writing {} bytes",
611                                      data.length());
612                     conn.close("Buffer overrun");
613                     return;
614                 }
615 
616                 size_t copied = boost::asio::buffer_copy(
617                     handler->inputBuffer.prepare(data.size()),
618                     boost::asio::buffer(data));
619                 handler->inputBuffer.commit(copied);
620                 handler->doWrite();
621             });
622     }
623 }
624 
625 } // namespace obmc_vm
626 
627 } // namespace crow
628