xref: /openbmc/bmcweb/include/vm_websocket.hpp (revision 4cee35e7)
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 = 131088;
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
123                                  << " bytes";
124                 if (ec)
125                 {
126                     BMCWEB_LOG_ERROR << "Couldn't read from VM port: " << ec;
127                     if (session != nullptr)
128                     {
129                         session->close("Error in connecting to VM port");
130                     }
131                     return;
132                 }
133                 if (session == nullptr)
134                 {
135                     return;
136                 }
137 
138                 outputBuffer->commit(bytesRead);
139                 std::string_view payload(
140                     static_cast<const char*>(outputBuffer->data().data()),
141                     bytesRead);
142                 session->sendBinary(payload);
143                 outputBuffer->consume(bytesRead);
144 
145                 doRead();
146             });
147     }
148 
149     boost::process::async_pipe pipeOut;
150     boost::process::async_pipe pipeIn;
151     boost::process::child proxy;
152     std::string media;
153     bool doingWrite{false};
154 
155     std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>>
156         outputBuffer;
157     std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>>
158         inputBuffer;
159 };
160 
161 static std::shared_ptr<Handler> handler;
162 
163 inline void requestRoutes(App& app)
164 {
165     BMCWEB_ROUTE(app, "/vm/0/0")
166         .privileges({{"ConfigureComponents", "ConfigureManager"}})
167         .websocket()
168         .onopen([](crow::websocket::Connection& conn) {
169             BMCWEB_LOG_DEBUG << "Connection " << &conn << " opened";
170 
171             if (session != nullptr)
172             {
173                 conn.close("Session already connected");
174                 return;
175             }
176 
177             if (handler != nullptr)
178             {
179                 conn.close("Handler already running");
180                 return;
181             }
182 
183             session = &conn;
184 
185             // media is the last digit of the endpoint /vm/0/0. A future
186             // enhancement can include supporting different endpoint values.
187             const char* media = "0";
188             handler = std::make_shared<Handler>(media, conn.getIoContext());
189             handler->connect();
190         })
191         .onclose([](crow::websocket::Connection& conn,
192                     const std::string& /*reason*/) {
193             if (&conn != session)
194             {
195                 return;
196             }
197 
198             session = nullptr;
199             handler->doClose();
200             handler->inputBuffer->clear();
201             handler->outputBuffer->clear();
202             handler.reset();
203         })
204         .onmessage([](crow::websocket::Connection& conn,
205                       const std::string& data, bool) {
206             if (data.length() > handler->inputBuffer->capacity())
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