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