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