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