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