xref: /openbmc/bmcweb/include/vm_websocket.hpp (revision 3ccb3adb)
1 #pragma once
2 
3 #include "app.hpp"
4 
5 #include <boost/beast/core/flat_static_buffer.hpp>
6 #include <boost/process/async_pipe.hpp>
7 #include <boost/process/child.hpp>
8 #include <boost/process/io.hpp>
9 #include <websocket.hpp>
10 
11 #include <csignal>
12 
13 namespace crow
14 {
15 namespace obmc_vm
16 {
17 
18 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
19 static crow::websocket::Connection* session = nullptr;
20 
21 // The max network block device buffer size is 128kb plus 16bytes
22 // for the message header:
23 // https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md#simple-reply-message
24 static constexpr auto nbdBufferSize = (128 * 1024 + 16) * 4;
25 
26 class Handler : public std::enable_shared_from_this<Handler>
27 {
28   public:
29     Handler(const std::string& mediaIn, boost::asio::io_context& ios) :
30         pipeOut(ios), pipeIn(ios), media(mediaIn),
31         outputBuffer(new boost::beast::flat_static_buffer<nbdBufferSize>),
32         inputBuffer(new boost::beast::flat_static_buffer<nbdBufferSize>)
33     {}
34 
35     ~Handler() = default;
36 
37     Handler(const Handler&) = delete;
38     Handler(Handler&&) = delete;
39     Handler& operator=(const Handler&) = delete;
40     Handler& operator=(Handler&&) = delete;
41 
42     void doClose()
43     {
44         // boost::process::child::terminate uses SIGKILL, need to send SIGTERM
45         // to allow the proxy to stop nbd-client and the USB device gadget.
46         int rc = kill(proxy.id(), SIGTERM);
47         if (rc != 0)
48         {
49             return;
50         }
51         proxy.wait();
52     }
53 
54     void connect()
55     {
56         std::error_code ec;
57         proxy = boost::process::child("/usr/sbin/nbd-proxy", media,
58                                       boost::process::std_out > pipeOut,
59                                       boost::process::std_in < pipeIn, ec);
60         if (ec)
61         {
62             BMCWEB_LOG_ERROR << "Couldn't connect to nbd-proxy: "
63                              << ec.message();
64             if (session != nullptr)
65             {
66                 session->close("Error connecting to nbd-proxy");
67             }
68             return;
69         }
70         doWrite();
71         doRead();
72     }
73 
74     void doWrite()
75     {
76         if (doingWrite)
77         {
78             BMCWEB_LOG_DEBUG << "Already writing.  Bailing out";
79             return;
80         }
81 
82         if (inputBuffer->size() == 0)
83         {
84             BMCWEB_LOG_DEBUG << "inputBuffer empty.  Bailing out";
85             return;
86         }
87 
88         doingWrite = true;
89         pipeIn.async_write_some(
90             inputBuffer->data(),
91             [this, self(shared_from_this())](boost::beast::error_code ec,
92                                              std::size_t bytesWritten) {
93             BMCWEB_LOG_DEBUG << "Wrote " << bytesWritten << "bytes";
94             doingWrite = false;
95             inputBuffer->consume(bytesWritten);
96 
97             if (session == nullptr)
98             {
99                 return;
100             }
101             if (ec == boost::asio::error::eof)
102             {
103                 session->close("VM socket port closed");
104                 return;
105             }
106             if (ec)
107             {
108                 session->close("Error in writing to proxy port");
109                 BMCWEB_LOG_ERROR << "Error in VM socket write " << ec;
110                 return;
111             }
112             doWrite();
113             });
114     }
115 
116     void doRead()
117     {
118         std::size_t bytes = outputBuffer->capacity() - outputBuffer->size();
119 
120         pipeOut.async_read_some(
121             outputBuffer->prepare(bytes),
122             [this, self(shared_from_this())](
123                 const boost::system::error_code& ec, std::size_t bytesRead) {
124             BMCWEB_LOG_DEBUG << "Read done.  Read " << bytesRead << " bytes";
125             if (ec)
126             {
127                 BMCWEB_LOG_ERROR << "Couldn't read from VM port: " << ec;
128                 if (session != nullptr)
129                 {
130                     session->close("Error in connecting to VM port");
131                 }
132                 return;
133             }
134             if (session == nullptr)
135             {
136                 return;
137             }
138 
139             outputBuffer->commit(bytesRead);
140             std::string_view payload(
141                 static_cast<const char*>(outputBuffer->data().data()),
142                 bytesRead);
143             session->sendBinary(payload);
144             outputBuffer->consume(bytesRead);
145 
146             doRead();
147             });
148     }
149 
150     boost::process::async_pipe pipeOut;
151     boost::process::async_pipe pipeIn;
152     boost::process::child proxy;
153     std::string media;
154     bool doingWrite{false};
155 
156     std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>>
157         outputBuffer;
158     std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>>
159         inputBuffer;
160 };
161 
162 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
163 static std::shared_ptr<Handler> handler;
164 
165 inline void requestRoutes(App& app)
166 {
167     BMCWEB_ROUTE(app, "/vm/0/0")
168         .privileges({{"ConfigureComponents", "ConfigureManager"}})
169         .websocket()
170         .onopen([](crow::websocket::Connection& conn) {
171             BMCWEB_LOG_DEBUG << "Connection " << &conn << " opened";
172 
173             if (session != nullptr)
174             {
175                 conn.close("Session already connected");
176                 return;
177             }
178 
179             if (handler != nullptr)
180             {
181                 conn.close("Handler already running");
182                 return;
183             }
184 
185             session = &conn;
186 
187             // media is the last digit of the endpoint /vm/0/0. A future
188             // enhancement can include supporting different endpoint values.
189             const char* media = "0";
190             handler = std::make_shared<Handler>(media, conn.getIoContext());
191             handler->connect();
192         })
193         .onclose([](crow::websocket::Connection& conn,
194                     const std::string& /*reason*/) {
195             if (&conn != session)
196             {
197                 return;
198             }
199 
200             session = nullptr;
201             handler->doClose();
202             handler->inputBuffer->clear();
203             handler->outputBuffer->clear();
204             handler.reset();
205         })
206         .onmessage([](crow::websocket::Connection& conn,
207                       const std::string& data, bool) {
208             if (data.length() >
209                 handler->inputBuffer->capacity() - handler->inputBuffer->size())
210             {
211                 BMCWEB_LOG_ERROR << "Buffer overrun when writing "
212                                  << data.length() << " bytes";
213                 conn.close("Buffer overrun");
214                 return;
215             }
216 
217             boost::asio::buffer_copy(handler->inputBuffer->prepare(data.size()),
218                                      boost::asio::buffer(data));
219             handler->inputBuffer->commit(data.size());
220             handler->doWrite();
221         });
222 }
223 
224 } // namespace obmc_vm
225 } // namespace crow
226