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