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