xref: /openbmc/bmcweb/include/vm_websocket.hpp (revision 3bfa3b29c0515a9e77c7c69fe072b7ff2e0fc302)
1 #pragma once
2 
3 #include "app.hpp"
4 #include "websocket.hpp"
5 
6 #include <boost/asio/readable_pipe.hpp>
7 #include <boost/asio/writable_pipe.hpp>
8 #include <boost/beast/core/flat_static_buffer.hpp>
9 #include <boost/process/v2/process.hpp>
10 #include <boost/process/v2/stdio.hpp>
11 
12 #include <csignal>
13 
14 namespace crow
15 {
16 namespace obmc_vm
17 {
18 
19 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
20 static crow::websocket::Connection* session = nullptr;
21 
22 // The max network block device buffer size is 128kb plus 16bytes
23 // for the message header:
24 // https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md#simple-reply-message
25 static constexpr auto nbdBufferSize = (128 * 1024 + 16) * 4;
26 
27 class Handler : public std::enable_shared_from_this<Handler>
28 {
29   public:
30     Handler(const std::string& media, boost::asio::io_context& ios) :
31         pipeOut(ios), pipeIn(ios),
32         proxy(ios, "/usr/bin/nbd-proxy", {media},
33               boost::process::v2::process_stdio{
34                   .in = pipeIn, .out = pipeOut, .err = nullptr}),
35         outputBuffer(new boost::beast::flat_static_buffer<nbdBufferSize>),
36         inputBuffer(new boost::beast::flat_static_buffer<nbdBufferSize>)
37     {}
38 
39     ~Handler() = default;
40 
41     Handler(const Handler&) = delete;
42     Handler(Handler&&) = delete;
43     Handler& operator=(const Handler&) = delete;
44     Handler& operator=(Handler&&) = delete;
45 
46     void doClose()
47     {
48         // boost::process::child::terminate uses SIGKILL, need to send SIGTERM
49         // to allow the proxy to stop nbd-client and the USB device gadget.
50         int rc = kill(proxy.id(), SIGTERM);
51         if (rc != 0)
52         {
53             BMCWEB_LOG_ERROR("Failed to terminate nbd-proxy: {}", errno);
54             return;
55         }
56 
57         proxy.wait();
58     }
59 
60     void connect()
61     {
62         std::error_code ec;
63         if (ec)
64         {
65             BMCWEB_LOG_ERROR("Couldn't connect to nbd-proxy: {}", 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 {}bytes", bytesWritten);
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 {} bytes", bytesRead);
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::asio::readable_pipe pipeOut;
153     boost::asio::writable_pipe pipeIn;
154     boost::process::v2::process proxy;
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         size_t copied =
219             boost::asio::buffer_copy(handler->inputBuffer->prepare(data.size()),
220                                      boost::asio::buffer(data));
221         handler->inputBuffer->commit(copied);
222         handler->doWrite();
223     });
224 }
225 
226 } // namespace obmc_vm
227 } // namespace crow
228