1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 #pragma once 4 #include "app.hpp" 5 #include "async_resp.hpp" 6 #include "websocket.hpp" 7 8 #include <sys/socket.h> 9 10 #include <boost/asio/local/stream_protocol.hpp> 11 #include <boost/container/flat_map.hpp> 12 #include <boost/system/error_code.hpp> 13 14 #include <array> 15 #include <memory> 16 #include <string> 17 #include <string_view> 18 19 namespace crow 20 { 21 namespace obmc_console 22 { 23 24 // Update this value each time we add new console route. 25 static constexpr const uint maxSessions = 32; 26 27 class ConsoleHandler : public std::enable_shared_from_this<ConsoleHandler> 28 { 29 public: 30 ConsoleHandler(boost::asio::io_context& ioc, 31 crow::websocket::Connection& connIn) : 32 hostSocket(ioc), conn(connIn) 33 {} 34 35 ~ConsoleHandler() = default; 36 37 ConsoleHandler(const ConsoleHandler&) = delete; 38 ConsoleHandler(ConsoleHandler&&) = delete; 39 ConsoleHandler& operator=(const ConsoleHandler&) = delete; 40 ConsoleHandler& operator=(ConsoleHandler&&) = delete; 41 42 void doWrite() 43 { 44 if (doingWrite) 45 { 46 BMCWEB_LOG_DEBUG("Already writing. Bailing out"); 47 return; 48 } 49 50 if (inputBuffer.empty()) 51 { 52 BMCWEB_LOG_DEBUG("Outbuffer empty. Bailing out"); 53 return; 54 } 55 56 doingWrite = true; 57 hostSocket.async_write_some( 58 boost::asio::buffer(inputBuffer.data(), inputBuffer.size()), 59 [weak(weak_from_this())](const boost::beast::error_code& ec, 60 std::size_t bytesWritten) { 61 std::shared_ptr<ConsoleHandler> self = weak.lock(); 62 if (self == nullptr) 63 { 64 return; 65 } 66 67 self->doingWrite = false; 68 self->inputBuffer.erase(0, bytesWritten); 69 70 if (ec == boost::asio::error::eof) 71 { 72 self->conn.close("Error in reading to host port"); 73 return; 74 } 75 if (ec) 76 { 77 BMCWEB_LOG_ERROR("Error in host serial write {}", 78 ec.message()); 79 return; 80 } 81 self->doWrite(); 82 }); 83 } 84 85 static void afterSendEx(const std::weak_ptr<ConsoleHandler>& weak) 86 { 87 std::shared_ptr<ConsoleHandler> self = weak.lock(); 88 if (self == nullptr) 89 { 90 return; 91 } 92 self->doRead(); 93 } 94 95 void doRead() 96 { 97 BMCWEB_LOG_DEBUG("Reading from socket"); 98 hostSocket.async_read_some( 99 boost::asio::buffer(outputBuffer), 100 [this, weakSelf(weak_from_this())]( 101 const boost::system::error_code& ec, std::size_t bytesRead) { 102 BMCWEB_LOG_DEBUG("read done. Read {} bytes", bytesRead); 103 std::shared_ptr<ConsoleHandler> self = weakSelf.lock(); 104 if (self == nullptr) 105 { 106 return; 107 } 108 if (ec) 109 { 110 BMCWEB_LOG_ERROR("Couldn't read from host serial port: {}", 111 ec.message()); 112 conn.close("Error connecting to host port"); 113 return; 114 } 115 std::string_view payload(outputBuffer.data(), bytesRead); 116 self->conn.sendEx( 117 crow::websocket::MessageType::Binary, payload, 118 std::bind_front(afterSendEx, weak_from_this())); 119 }); 120 } 121 122 bool connect(int fd) 123 { 124 boost::system::error_code ec; 125 boost::asio::local::stream_protocol proto; 126 127 hostSocket.assign(proto, fd, ec); 128 129 if (ec) 130 { 131 BMCWEB_LOG_ERROR( 132 "Failed to assign the DBUS socket Socket assign error: {}", 133 ec.message()); 134 return false; 135 } 136 137 conn.resumeRead(); 138 doWrite(); 139 doRead(); 140 return true; 141 } 142 143 boost::asio::local::stream_protocol::socket hostSocket; 144 145 std::array<char, 4096> outputBuffer{}; 146 147 std::string inputBuffer; 148 bool doingWrite = false; 149 crow::websocket::Connection& conn; 150 }; 151 152 using ObmcConsoleMap = boost::container::flat_map< 153 crow::websocket::Connection*, std::shared_ptr<ConsoleHandler>, std::less<>, 154 std::vector<std::pair<crow::websocket::Connection*, 155 std::shared_ptr<ConsoleHandler>>>>; 156 157 inline ObmcConsoleMap& getConsoleHandlerMap() 158 { 159 static ObmcConsoleMap map; 160 return map; 161 } 162 163 // Remove connection from the connection map and if connection map is empty 164 // then remove the handler from handlers map. 165 inline void onClose(crow::websocket::Connection& conn, const std::string& err) 166 { 167 BMCWEB_LOG_INFO("Closing websocket. Reason: {}", err); 168 169 auto iter = getConsoleHandlerMap().find(&conn); 170 if (iter == getConsoleHandlerMap().end()) 171 { 172 BMCWEB_LOG_CRITICAL("Unable to find connection {}", logPtr(&conn)); 173 return; 174 } 175 BMCWEB_LOG_DEBUG("Remove connection {} from obmc console", logPtr(&conn)); 176 177 // Removed last connection so remove the path 178 getConsoleHandlerMap().erase(iter); 179 } 180 181 inline void connectConsoleSocket(crow::websocket::Connection& conn, 182 const boost::system::error_code& ec, 183 const sdbusplus::message::unix_fd& unixfd) 184 { 185 if (ec) 186 { 187 BMCWEB_LOG_ERROR( 188 "Failed to call console Connect() method DBUS error: {}", 189 ec.message()); 190 conn.close("Failed to connect"); 191 return; 192 } 193 194 // Look up the handler 195 auto iter = getConsoleHandlerMap().find(&conn); 196 if (iter == getConsoleHandlerMap().end()) 197 { 198 BMCWEB_LOG_ERROR("Connection was already closed"); 199 return; 200 } 201 202 int fd = dup(unixfd); 203 if (fd == -1) 204 { 205 BMCWEB_LOG_ERROR("Failed to dup the DBUS unixfd error"); 206 conn.close("Internal error"); 207 return; 208 } 209 210 BMCWEB_LOG_DEBUG("Console duped FD: {}", fd); 211 212 if (!iter->second->connect(fd)) 213 { 214 close(fd); 215 conn.close("Internal Error"); 216 } 217 } 218 219 inline void processConsoleObject( 220 crow::websocket::Connection& conn, const std::string& consoleObjPath, 221 const boost::system::error_code& ec, 222 const ::dbus::utility::MapperGetObject& objInfo) 223 { 224 // Look up the handler 225 auto iter = getConsoleHandlerMap().find(&conn); 226 if (iter == getConsoleHandlerMap().end()) 227 { 228 BMCWEB_LOG_ERROR("Connection was already closed"); 229 return; 230 } 231 232 if (ec) 233 { 234 BMCWEB_LOG_WARNING("getDbusObject() for consoles failed. DBUS error:{}", 235 ec.message()); 236 conn.close("getDbusObject() for consoles failed."); 237 return; 238 } 239 240 const auto valueIface = objInfo.begin(); 241 if (valueIface == objInfo.end()) 242 { 243 BMCWEB_LOG_WARNING("getDbusObject() returned unexpected size: {}", 244 objInfo.size()); 245 conn.close("getDbusObject() returned unexpected size"); 246 return; 247 } 248 249 const std::string& consoleService = valueIface->first; 250 BMCWEB_LOG_DEBUG("Looking up unixFD for Service {} Path {}", consoleService, 251 consoleObjPath); 252 // Call Connect() method to get the unix FD 253 crow::connections::systemBus->async_method_call( 254 [&conn](const boost::system::error_code& ec1, 255 const sdbusplus::message::unix_fd& unixfd) { 256 connectConsoleSocket(conn, ec1, unixfd); 257 }, 258 consoleService, consoleObjPath, "xyz.openbmc_project.Console.Access", 259 "Connect"); 260 } 261 262 // Query consoles from DBUS and find the matching to the 263 // rules string. 264 inline void onOpen(crow::websocket::Connection& conn) 265 { 266 std::string consoleLeaf; 267 268 BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn)); 269 270 if (getConsoleHandlerMap().size() >= maxSessions) 271 { 272 conn.close("Max sessions are already connected"); 273 return; 274 } 275 276 std::shared_ptr<ConsoleHandler> handler = 277 std::make_shared<ConsoleHandler>(conn.getIoContext(), conn); 278 getConsoleHandlerMap().emplace(&conn, handler); 279 280 conn.deferRead(); 281 282 // Keep old path for backward compatibility 283 if (conn.url().path() == "/console0") 284 { 285 consoleLeaf = "default"; 286 } 287 else 288 { 289 // Get the console id from console router path and prepare the console 290 // object path and console service. 291 consoleLeaf = conn.url().segments().back(); 292 } 293 std::string consolePath = 294 sdbusplus::message::object_path("/xyz/openbmc_project/console") / 295 consoleLeaf; 296 297 BMCWEB_LOG_DEBUG("Console Object path = {} Request target = {}", 298 consolePath, conn.url().path()); 299 300 // mapper call lambda 301 constexpr std::array<std::string_view, 1> interfaces = { 302 "xyz.openbmc_project.Console.Access"}; 303 304 dbus::utility::getDbusObject( 305 consolePath, interfaces, 306 [&conn, consolePath](const boost::system::error_code& ec, 307 const ::dbus::utility::MapperGetObject& objInfo) { 308 processConsoleObject(conn, consolePath, ec, objInfo); 309 }); 310 } 311 312 inline void onMessage(crow::websocket::Connection& conn, 313 const std::string& data, bool /*isBinary*/) 314 { 315 auto handler = getConsoleHandlerMap().find(&conn); 316 if (handler == getConsoleHandlerMap().end()) 317 { 318 BMCWEB_LOG_CRITICAL("Unable to find connection {}", logPtr(&conn)); 319 return; 320 } 321 handler->second->inputBuffer += data; 322 handler->second->doWrite(); 323 } 324 325 inline void requestRoutes(App& app) 326 { 327 BMCWEB_ROUTE(app, "/console0") 328 .privileges({{"OpenBMCHostConsole"}}) 329 .websocket() 330 .onopen(onOpen) 331 .onclose(onClose) 332 .onmessage(onMessage); 333 334 BMCWEB_ROUTE(app, "/console/<str>") 335 .privileges({{"OpenBMCHostConsole"}}) 336 .websocket() 337 .onopen(onOpen) 338 .onclose(onClose) 339 .onmessage(onMessage); 340 } 341 } // namespace obmc_console 342 } // namespace crow 343