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