xref: /openbmc/bmcweb/include/obmc_console.hpp (revision 177612aaa0633cf9d5aef0b763a43135cf552d9b)
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:
ConsoleHandler(boost::asio::io_context & ioc,crow::websocket::Connection & connIn)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 
doWrite()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 
afterSendEx(const std::weak_ptr<ConsoleHandler> & weak)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 
doRead()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 
connect(int fd)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 
getConsoleHandlerMap()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.
onClose(crow::websocket::Connection & conn,const std::string & err)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 
connectConsoleSocket(crow::websocket::Connection & conn,const boost::system::error_code & ec,const sdbusplus::message::unix_fd & unixfd)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 
processConsoleObject(crow::websocket::Connection & conn,const std::string & consoleObjPath,const boost::system::error_code & ec,const::dbus::utility::MapperGetObject & objInfo)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.
onOpen(crow::websocket::Connection & conn)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 
onMessage(crow::websocket::Connection & conn,const std::string & data,bool)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 
requestRoutes(App & app)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