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