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