xref: /openbmc/bmcweb/include/vm_websocket.hpp (revision a3b9eb98)
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:
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         outputBuffer(new boost::beast::flat_static_buffer<nbdBufferSize>),
45         inputBuffer(new boost::beast::flat_static_buffer<nbdBufferSize>)
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 
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 
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 
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 
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     std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>>
167         outputBuffer;
168     std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>>
169         inputBuffer;
170 };
171 
172 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
173 static std::shared_ptr<Handler> handler;
174 
175 } // namespace obmc_vm
176 
177 namespace nbd_proxy
178 {
179 using boost::asio::local::stream_protocol;
180 
181 // The max network block device buffer size is 128kb plus 16bytes
182 // for the message header:
183 // https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md#simple-reply-message
184 static constexpr auto nbdBufferSize = (128 * 1024 + 16) * 4;
185 
186 struct NbdProxyServer : std::enable_shared_from_this<NbdProxyServer>
187 {
188     NbdProxyServer(crow::websocket::Connection& connIn,
189                    const std::string& socketIdIn,
190                    const std::string& endpointIdIn, const std::string& pathIn) :
191         socketId(socketIdIn),
192         endpointId(endpointIdIn), path(pathIn),
193 
194         peerSocket(connIn.getIoContext()),
195         acceptor(connIn.getIoContext(), stream_protocol::endpoint(socketId)),
196         connection(connIn)
197     {}
198 
199     NbdProxyServer(const NbdProxyServer&) = delete;
200     NbdProxyServer(NbdProxyServer&&) = delete;
201     NbdProxyServer& operator=(const NbdProxyServer&) = delete;
202     NbdProxyServer& operator=(NbdProxyServer&&) = delete;
203 
204     ~NbdProxyServer()
205     {
206         BMCWEB_LOG_DEBUG("NbdProxyServer destructor");
207 
208         BMCWEB_LOG_DEBUG("peerSocket->close()");
209         boost::system::error_code ec;
210         peerSocket.close(ec);
211 
212         BMCWEB_LOG_DEBUG("std::filesystem::remove({})", socketId);
213         std::error_code ec2;
214         std::filesystem::remove(socketId.c_str(), ec2);
215         if (ec2)
216         {
217             BMCWEB_LOG_DEBUG("Failed to remove file, ignoring");
218         }
219 
220         crow::connections::systemBus->async_method_call(
221             dbus::utility::logError, "xyz.openbmc_project.VirtualMedia", path,
222             "xyz.openbmc_project.VirtualMedia.Proxy", "Unmount");
223     }
224 
225     std::string getEndpointId() const
226     {
227         return endpointId;
228     }
229 
230     static void afterMount(const std::weak_ptr<NbdProxyServer>& weak,
231                            const boost::system::error_code& ec,
232                            bool /*isBinary*/)
233     {
234         std::shared_ptr<NbdProxyServer> self = weak.lock();
235         if (self == nullptr)
236         {
237             return;
238         }
239         if (ec)
240         {
241             BMCWEB_LOG_ERROR("DBus error: cannot call mount method = {}",
242                              ec.message());
243 
244             self->connection.close("Failed to mount media");
245             return;
246         }
247     }
248 
249     static void afterAccept(const std::weak_ptr<NbdProxyServer>& weak,
250                             const boost::system::error_code& ec,
251                             stream_protocol::socket socket)
252     {
253         if (ec)
254         {
255             BMCWEB_LOG_ERROR("UNIX socket: async_accept error = {}",
256                              ec.message());
257             return;
258         }
259 
260         BMCWEB_LOG_DEBUG("Connection opened");
261         std::shared_ptr<NbdProxyServer> self = weak.lock();
262         if (self == nullptr)
263         {
264             return;
265         }
266 
267         self->connection.resumeRead();
268         self->peerSocket = std::move(socket);
269         //  Start reading from socket
270         self->doRead();
271     }
272 
273     void run()
274     {
275         acceptor.async_accept(
276             std::bind_front(&NbdProxyServer::afterAccept, weak_from_this()));
277 
278         crow::connections::systemBus->async_method_call(
279             [weak{weak_from_this()}](const boost::system::error_code& ec,
280                                      bool isBinary) {
281             afterMount(weak, ec, isBinary);
282         },
283             "xyz.openbmc_project.VirtualMedia", path,
284             "xyz.openbmc_project.VirtualMedia.Proxy", "Mount");
285     }
286 
287     void send(std::string_view buffer, std::function<void()>&& onDone)
288     {
289         size_t copied = boost::asio::buffer_copy(
290             ws2uxBuf.prepare(buffer.size()), boost::asio::buffer(buffer));
291         ws2uxBuf.commit(copied);
292 
293         doWrite(std::move(onDone));
294     }
295 
296   private:
297     static void afterSendEx(const std::weak_ptr<NbdProxyServer>& weak)
298     {
299         std::shared_ptr<NbdProxyServer> self2 = weak.lock();
300         if (self2 != nullptr)
301         {
302             self2->ux2wsBuf.consume(self2->ux2wsBuf.size());
303             self2->doRead();
304         }
305     }
306 
307     inline void afterRead(const std::weak_ptr<NbdProxyServer>& weak,
308                           const boost::system::error_code& ec, size_t bytesRead)
309     {
310         if (ec)
311         {
312             BMCWEB_LOG_ERROR("UNIX socket: async_read_some error = {}",
313                              ec.message());
314             return;
315         }
316         std::shared_ptr<NbdProxyServer> self = weak.lock();
317         if (self == nullptr)
318         {
319             return;
320         }
321 
322         // Send to websocket
323         self->ux2wsBuf.commit(bytesRead);
324         self->connection.sendEx(
325             crow::websocket::MessageType::Binary,
326             boost::beast::buffers_to_string(self->ux2wsBuf.data()),
327             std::bind_front(&NbdProxyServer::afterSendEx, weak_from_this()));
328     }
329 
330     void doRead()
331     {
332         // Trigger async read
333         peerSocket.async_read_some(ux2wsBuf.prepare(nbdBufferSize),
334                                    std::bind_front(&NbdProxyServer::afterRead,
335                                                    this, weak_from_this()));
336     }
337 
338     static void afterWrite(const std::weak_ptr<NbdProxyServer>& weak,
339                            std::function<void()>&& onDone,
340                            const boost::system::error_code& ec,
341                            size_t bytesWritten)
342     {
343         std::shared_ptr<NbdProxyServer> self = weak.lock();
344         if (self == nullptr)
345         {
346             return;
347         }
348 
349         self->ws2uxBuf.consume(bytesWritten);
350         self->uxWriteInProgress = false;
351 
352         if (ec)
353         {
354             BMCWEB_LOG_ERROR("UNIX: async_write error = {}", ec.message());
355             self->connection.close("Internal error");
356             return;
357         }
358 
359         // Retrigger doWrite if there is something in buffer
360         if (self->ws2uxBuf.size() > 0)
361         {
362             self->doWrite(std::move(onDone));
363             return;
364         }
365         onDone();
366     }
367 
368     void doWrite(std::function<void()>&& onDone)
369     {
370         if (uxWriteInProgress)
371         {
372             BMCWEB_LOG_ERROR("Write in progress");
373             return;
374         }
375 
376         if (ws2uxBuf.size() == 0)
377         {
378             BMCWEB_LOG_ERROR("No data to write to UNIX socket");
379             return;
380         }
381 
382         uxWriteInProgress = true;
383         peerSocket.async_write_some(ws2uxBuf.data(),
384                                     std::bind_front(&NbdProxyServer::afterWrite,
385                                                     weak_from_this(),
386                                                     std::move(onDone)));
387     }
388 
389     // Keeps UNIX socket endpoint file path
390     const std::string socketId;
391     const std::string endpointId;
392     const std::string path;
393 
394     bool uxWriteInProgress = false;
395 
396     // UNIX => WebSocket buffer
397     boost::beast::flat_static_buffer<nbdBufferSize> ux2wsBuf;
398 
399     // WebSocket => UNIX buffer
400     boost::beast::flat_static_buffer<nbdBufferSize> ws2uxBuf;
401 
402     // The socket used to communicate with the client.
403     stream_protocol::socket peerSocket;
404 
405     // Default acceptor for UNIX socket
406     stream_protocol::acceptor acceptor;
407 
408     crow::websocket::Connection& connection;
409 };
410 
411 using SessionMap = boost::container::flat_map<crow::websocket::Connection*,
412                                               std::shared_ptr<NbdProxyServer>>;
413 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
414 static SessionMap sessions;
415 
416 inline void
417     afterGetSocket(crow::websocket::Connection& conn,
418                    const sdbusplus::message::object_path& path,
419                    const boost::system::error_code& ec,
420                    const dbus::utility::DBusPropertiesMap& propertiesList)
421 {
422     if (ec)
423     {
424         BMCWEB_LOG_ERROR("DBus getAllProperties error: {}", ec.message());
425         conn.close("Internal Error");
426         return;
427     }
428     std::string endpointId;
429     std::string socket;
430 
431     bool success = sdbusplus::unpackPropertiesNoThrow(
432         redfish::dbus_utils::UnpackErrorPrinter(), propertiesList, "EndpointId",
433         endpointId, "Socket", socket);
434 
435     if (!success)
436     {
437         BMCWEB_LOG_ERROR("Failed to unpack properties");
438         conn.close("Internal Error");
439         return;
440     }
441 
442     for (const auto& session : sessions)
443     {
444         if (session.second->getEndpointId() == conn.url().path())
445         {
446             BMCWEB_LOG_ERROR("Cannot open new connection - socket is in use");
447             conn.close("Slot is in use");
448             return;
449         }
450     }
451 
452     // If the socket file exists (i.e. after bmcweb crash),
453     // we cannot reuse it.
454     std::error_code ec2;
455     std::filesystem::remove(socket.c_str(), ec2);
456     // Ignore failures.  File might not exist.
457 
458     sessions[&conn] = std::make_shared<NbdProxyServer>(conn, socket, endpointId,
459                                                        path);
460     sessions[&conn]->run();
461 }
462 
463 inline void onOpen(crow::websocket::Connection& conn)
464 {
465     BMCWEB_LOG_DEBUG("nbd-proxy.onopen({})", logPtr(&conn));
466 
467     if (conn.url().segments().size() < 2)
468     {
469         BMCWEB_LOG_ERROR("Invalid path - \"{}\"", conn.url().path());
470         conn.close("Internal error");
471         return;
472     }
473 
474     std::string index = conn.url().segments().back();
475     std::string path =
476         std::format("/xyz/openbmc_project/VirtualMedia/Proxy/Slot_{}", index);
477 
478     sdbusplus::asio::getAllProperties(
479         *crow::connections::systemBus, "xyz.openbmc_project.VirtualMedia", path,
480         "xyz.openbmc_project.VirtualMedia.MountPoint",
481         [&conn, path](const boost::system::error_code& ec,
482                       const dbus::utility::DBusPropertiesMap& propertiesList) {
483         afterGetSocket(conn, path, ec, propertiesList);
484     });
485 
486     // We need to wait for dbus and the websockets to hook up before data is
487     // sent/received.  Tell the core to hold off messages until the sockets are
488     // up
489     conn.deferRead();
490 }
491 
492 inline void onClose(crow::websocket::Connection& conn,
493                     const std::string& reason)
494 {
495     BMCWEB_LOG_DEBUG("nbd-proxy.onclose(reason = '{}')", reason);
496     auto session = sessions.find(&conn);
497     if (session == sessions.end())
498     {
499         BMCWEB_LOG_DEBUG("No session to close");
500         return;
501     }
502     // Remove reference to session in global map
503     sessions.erase(session);
504 }
505 
506 inline void onMessage(crow::websocket::Connection& conn, std::string_view data,
507                       crow::websocket::MessageType /*type*/,
508                       std::function<void()>&& whenComplete)
509 {
510     BMCWEB_LOG_DEBUG("nbd-proxy.onMessage(len = {})", data.size());
511 
512     // Acquire proxy from sessions
513     auto session = sessions.find(&conn);
514     if (session == sessions.end() || session->second == nullptr)
515     {
516         whenComplete();
517         return;
518     }
519 
520     session->second->send(data, std::move(whenComplete));
521 }
522 } // namespace nbd_proxy
523 
524 namespace obmc_vm
525 {
526 
527 inline void requestRoutes(App& app)
528 {
529     static_assert(
530         !(BMCWEB_VM_WEBSOCKET && BMCWEB_VM_NBDPROXY),
531         "nbd proxy cannot be turned on at the same time as vm websocket.");
532 
533     if constexpr (BMCWEB_VM_NBDPROXY)
534     {
535         BMCWEB_ROUTE(app, "/nbd/<str>")
536             .privileges({{"ConfigureComponents", "ConfigureManager"}})
537             .websocket()
538             .onopen(nbd_proxy::onOpen)
539             .onclose(nbd_proxy::onClose)
540             .onmessageex(nbd_proxy::onMessage);
541 
542         BMCWEB_ROUTE(app, "/vm/0/0")
543             .privileges({{"ConfigureComponents", "ConfigureManager"}})
544             .websocket()
545             .onopen(nbd_proxy::onOpen)
546             .onclose(nbd_proxy::onClose)
547             .onmessageex(nbd_proxy::onMessage);
548     }
549     if constexpr (BMCWEB_VM_WEBSOCKET)
550     {
551         BMCWEB_ROUTE(app, "/vm/0/0")
552             .privileges({{"ConfigureComponents", "ConfigureManager"}})
553             .websocket()
554             .onopen([](crow::websocket::Connection& conn) {
555             BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn));
556 
557             if (session != nullptr)
558             {
559                 conn.close("Session already connected");
560                 return;
561             }
562 
563             if (handler != nullptr)
564             {
565                 conn.close("Handler already running");
566                 return;
567             }
568 
569             session = &conn;
570 
571             // media is the last digit of the endpoint /vm/0/0. A future
572             // enhancement can include supporting different endpoint values.
573             const char* media = "0";
574             handler = std::make_shared<Handler>(media, conn.getIoContext());
575             handler->connect();
576         })
577             .onclose([](crow::websocket::Connection& conn,
578                         const std::string& /*reason*/) {
579             if (&conn != session)
580             {
581                 return;
582             }
583 
584             session = nullptr;
585             handler->doClose();
586             handler->inputBuffer->clear();
587             handler->outputBuffer->clear();
588             handler.reset();
589         })
590             .onmessage([](crow::websocket::Connection& conn,
591                           const std::string& data, bool) {
592             if (data.length() >
593                 handler->inputBuffer->capacity() - handler->inputBuffer->size())
594             {
595                 BMCWEB_LOG_ERROR("Buffer overrun when writing {} bytes",
596                                  data.length());
597                 conn.close("Buffer overrun");
598                 return;
599             }
600 
601             size_t copied = boost::asio::buffer_copy(
602                 handler->inputBuffer->prepare(data.size()),
603                 boost::asio::buffer(data));
604             handler->inputBuffer->commit(copied);
605             handler->doWrite();
606         });
607     }
608 }
609 
610 } // namespace obmc_vm
611 
612 } // namespace crow
613