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