xref: /openbmc/bmcweb/include/vm_websocket.hpp (revision e9cc1bc9)
1 #pragma once
2 
3 #include "app.hpp"
4 #include "websocket.hpp"
5 
6 #include <boost/beast/core/flat_static_buffer.hpp>
7 #include <boost/process/async_pipe.hpp>
8 #include <boost/process/child.hpp>
9 #include <boost/process/io.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             BMCWEB_LOG_ERROR("Failed to terminate nbd-proxy: {}", errno);
50             return;
51         }
52 
53         proxy.wait();
54     }
55 
56     void connect()
57     {
58         std::error_code ec;
59         proxy = boost::process::child("/usr/bin/nbd-proxy", media,
60                                       boost::process::std_out > pipeOut,
61                                       boost::process::std_in < pipeIn, ec);
62         if (ec)
63         {
64             BMCWEB_LOG_ERROR("Couldn't connect to nbd-proxy: {}", ec.message());
65             if (session != nullptr)
66             {
67                 session->close("Error connecting to nbd-proxy");
68             }
69             return;
70         }
71         doWrite();
72         doRead();
73     }
74 
75     void doWrite()
76     {
77         if (doingWrite)
78         {
79             BMCWEB_LOG_DEBUG("Already writing.  Bailing out");
80             return;
81         }
82 
83         if (inputBuffer->size() == 0)
84         {
85             BMCWEB_LOG_DEBUG("inputBuffer empty.  Bailing out");
86             return;
87         }
88 
89         doingWrite = true;
90         pipeIn.async_write_some(
91             inputBuffer->data(),
92             [this, self(shared_from_this())](const boost::beast::error_code& ec,
93                                              std::size_t bytesWritten) {
94             BMCWEB_LOG_DEBUG("Wrote {}bytes", bytesWritten);
95             doingWrite = false;
96             inputBuffer->consume(bytesWritten);
97 
98             if (session == nullptr)
99             {
100                 return;
101             }
102             if (ec == boost::asio::error::eof)
103             {
104                 session->close("VM socket port closed");
105                 return;
106             }
107             if (ec)
108             {
109                 session->close("Error in writing to proxy port");
110                 BMCWEB_LOG_ERROR("Error in VM socket write {}", ec);
111                 return;
112             }
113             doWrite();
114             });
115     }
116 
117     void doRead()
118     {
119         std::size_t bytes = outputBuffer->capacity() - outputBuffer->size();
120 
121         pipeOut.async_read_some(
122             outputBuffer->prepare(bytes),
123             [this, self(shared_from_this())](
124                 const boost::system::error_code& ec, std::size_t bytesRead) {
125             BMCWEB_LOG_DEBUG("Read done.  Read {} bytes", bytesRead);
126             if (ec)
127             {
128                 BMCWEB_LOG_ERROR("Couldn't read from VM port: {}", ec);
129                 if (session != nullptr)
130                 {
131                     session->close("Error in connecting to VM port");
132                 }
133                 return;
134             }
135             if (session == nullptr)
136             {
137                 return;
138             }
139 
140             outputBuffer->commit(bytesRead);
141             std::string_view payload(
142                 static_cast<const char*>(outputBuffer->data().data()),
143                 bytesRead);
144             session->sendBinary(payload);
145             outputBuffer->consume(bytesRead);
146 
147             doRead();
148             });
149     }
150 
151     boost::process::async_pipe pipeOut;
152     boost::process::async_pipe pipeIn;
153     boost::process::child proxy;
154     std::string media;
155     bool doingWrite{false};
156 
157     std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>>
158         outputBuffer;
159     std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>>
160         inputBuffer;
161 };
162 
163 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
164 static std::shared_ptr<Handler> handler;
165 
166 inline void requestRoutes(App& app)
167 {
168     BMCWEB_ROUTE(app, "/vm/0/0")
169         .privileges({{"ConfigureComponents", "ConfigureManager"}})
170         .websocket()
171         .onopen([](crow::websocket::Connection& conn) {
172             BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn));
173 
174             if (session != nullptr)
175             {
176                 conn.close("Session already connected");
177                 return;
178             }
179 
180             if (handler != nullptr)
181             {
182                 conn.close("Handler already running");
183                 return;
184             }
185 
186             session = &conn;
187 
188             // media is the last digit of the endpoint /vm/0/0. A future
189             // enhancement can include supporting different endpoint values.
190             const char* media = "0";
191             handler = std::make_shared<Handler>(media, conn.getIoContext());
192             handler->connect();
193         })
194         .onclose([](crow::websocket::Connection& conn,
195                     const std::string& /*reason*/) {
196             if (&conn != session)
197             {
198                 return;
199             }
200 
201             session = nullptr;
202             handler->doClose();
203             handler->inputBuffer->clear();
204             handler->outputBuffer->clear();
205             handler.reset();
206         })
207         .onmessage([](crow::websocket::Connection& conn,
208                       const std::string& data, bool) {
209             if (data.length() >
210                 handler->inputBuffer->capacity() - handler->inputBuffer->size())
211             {
212                 BMCWEB_LOG_ERROR("Buffer overrun when writing {} bytes",
213                                  data.length());
214                 conn.close("Buffer overrun");
215                 return;
216             }
217 
218             boost::asio::buffer_copy(handler->inputBuffer->prepare(data.size()),
219                                      boost::asio::buffer(data));
220             handler->inputBuffer->commit(data.size());
221             handler->doWrite();
222         });
223 }
224 
225 } // namespace obmc_vm
226 } // namespace crow
227