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