1 #pragma once 2 3 #include <crow/app.h> 4 #include <crow/websocket.h> 5 #include <signal.h> 6 7 #include <boost/beast/core/flat_static_buffer.hpp> 8 #include <boost/process.hpp> 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() 34 { 35 } 36 37 void doClose() 38 { 39 // boost::process::child::terminate uses SIGKILL, need to send SIGTERM 40 // to allow the proxy to stop nbd-client and the USB device gadget. 41 int rc = kill(proxy.id(), SIGTERM); 42 if (rc) 43 { 44 return; 45 } 46 proxy.wait(); 47 } 48 49 void connect() 50 { 51 std::error_code ec; 52 proxy = boost::process::child("/usr/sbin/nbd-proxy", media, 53 boost::process::std_out > pipeOut, 54 boost::process::std_in < pipeIn, ec); 55 if (ec) 56 { 57 BMCWEB_LOG_ERROR << "Couldn't connect to nbd-proxy: " 58 << ec.message(); 59 if (session != nullptr) 60 { 61 session->close("Error connecting to nbd-proxy"); 62 } 63 return; 64 } 65 doWrite(); 66 doRead(); 67 } 68 69 void doWrite() 70 { 71 if (doingWrite) 72 { 73 BMCWEB_LOG_DEBUG << "Already writing. Bailing out"; 74 return; 75 } 76 77 if (inputBuffer->size() == 0) 78 { 79 BMCWEB_LOG_DEBUG << "inputBuffer empty. Bailing out"; 80 return; 81 } 82 83 doingWrite = true; 84 pipeIn.async_write_some( 85 inputBuffer->data(), 86 [this, self(shared_from_this())](boost::beast::error_code ec, 87 std::size_t bytesWritten) { 88 BMCWEB_LOG_DEBUG << "Wrote " << bytesWritten << "bytes"; 89 doingWrite = false; 90 inputBuffer->consume(bytesWritten); 91 92 if (session == nullptr) 93 { 94 return; 95 } 96 if (ec == boost::asio::error::eof) 97 { 98 session->close("VM socket port closed"); 99 return; 100 } 101 if (ec) 102 { 103 session->close("Error in writing to proxy port"); 104 BMCWEB_LOG_ERROR << "Error in VM socket write " << ec; 105 return; 106 } 107 doWrite(); 108 }); 109 } 110 111 void doRead() 112 { 113 std::size_t bytes = outputBuffer->capacity() - outputBuffer->size(); 114 115 pipeOut.async_read_some( 116 outputBuffer->prepare(bytes), 117 [this, self(shared_from_this())]( 118 const boost::system::error_code& ec, std::size_t bytesRead) { 119 BMCWEB_LOG_DEBUG << "Read done. Read " << bytesRead 120 << " bytes"; 121 if (ec) 122 { 123 BMCWEB_LOG_ERROR << "Couldn't read from VM port: " << ec; 124 if (session != nullptr) 125 { 126 session->close("Error in connecting to VM port"); 127 } 128 return; 129 } 130 if (session == nullptr) 131 { 132 return; 133 } 134 135 outputBuffer->commit(bytesRead); 136 std::string_view payload( 137 static_cast<const char*>(outputBuffer->data().data()), 138 bytesRead); 139 session->sendBinary(payload); 140 outputBuffer->consume(bytesRead); 141 142 doRead(); 143 }); 144 } 145 146 boost::process::async_pipe pipeOut; 147 boost::process::async_pipe pipeIn; 148 boost::process::child proxy; 149 std::string media; 150 bool doingWrite; 151 152 std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>> 153 outputBuffer; 154 std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>> 155 inputBuffer; 156 }; 157 158 static std::shared_ptr<Handler> handler; 159 160 template <typename... Middlewares> void requestRoutes(Crow<Middlewares...>& app) 161 { 162 BMCWEB_ROUTE(app, "/vm/0/0") 163 .websocket() 164 .onopen([](crow::websocket::Connection& conn) { 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