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