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