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