xref: /openbmc/bmcweb/include/obmc_console.hpp (revision 8b901d78)
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),
31         conn(connIn)
32     {}
33 
34     ~ConsoleHandler() = default;
35 
36     ConsoleHandler(const ConsoleHandler&) = delete;
37     ConsoleHandler(ConsoleHandler&&) = delete;
38     ConsoleHandler& operator=(const ConsoleHandler&) = delete;
39     ConsoleHandler& operator=(ConsoleHandler&&) = delete;
40 
doWrite()41     void doWrite()
42     {
43         if (doingWrite)
44         {
45             BMCWEB_LOG_DEBUG("Already writing.  Bailing out");
46             return;
47         }
48 
49         if (inputBuffer.empty())
50         {
51             BMCWEB_LOG_DEBUG("Outbuffer empty.  Bailing out");
52             return;
53         }
54 
55         doingWrite = true;
56         hostSocket.async_write_some(
57             boost::asio::buffer(inputBuffer.data(), inputBuffer.size()),
58             [weak(weak_from_this())](const boost::beast::error_code& ec,
59                                      std::size_t bytesWritten) {
60             std::shared_ptr<ConsoleHandler> self = weak.lock();
61             if (self == nullptr)
62             {
63                 return;
64             }
65 
66             self->doingWrite = false;
67             self->inputBuffer.erase(0, bytesWritten);
68 
69             if (ec == boost::asio::error::eof)
70             {
71                 self->conn.close("Error in reading to host port");
72                 return;
73             }
74             if (ec)
75             {
76                 BMCWEB_LOG_ERROR("Error in host serial write {}", 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(crow::websocket::MessageType::Binary, payload,
115                               std::bind_front(afterSendEx, weak_from_this()));
116         });
117     }
118 
connect(int fd)119     bool connect(int fd)
120     {
121         boost::system::error_code ec;
122         boost::asio::local::stream_protocol proto;
123 
124         hostSocket.assign(proto, fd, ec);
125 
126         if (ec)
127         {
128             BMCWEB_LOG_ERROR(
129                 "Failed to assign the DBUS socket Socket assign error: {}",
130                 ec.message());
131             return false;
132         }
133 
134         conn.resumeRead();
135         doWrite();
136         doRead();
137         return true;
138     }
139 
140     boost::asio::local::stream_protocol::socket hostSocket;
141 
142     std::array<char, 4096> outputBuffer{};
143 
144     std::string inputBuffer;
145     bool doingWrite = false;
146     crow::websocket::Connection& conn;
147 };
148 
149 using ObmcConsoleMap = boost::container::flat_map<
150     crow::websocket::Connection*, std::shared_ptr<ConsoleHandler>, std::less<>,
151     std::vector<std::pair<crow::websocket::Connection*,
152                           std::shared_ptr<ConsoleHandler>>>>;
153 
getConsoleHandlerMap()154 inline ObmcConsoleMap& getConsoleHandlerMap()
155 {
156     static ObmcConsoleMap map;
157     return map;
158 }
159 
160 // Remove connection from the connection map and if connection map is empty
161 // then remove the handler from handlers map.
onClose(crow::websocket::Connection & conn,const std::string & err)162 inline void onClose(crow::websocket::Connection& conn, const std::string& err)
163 {
164     BMCWEB_LOG_INFO("Closing websocket. Reason: {}", err);
165 
166     auto iter = getConsoleHandlerMap().find(&conn);
167     if (iter == getConsoleHandlerMap().end())
168     {
169         BMCWEB_LOG_CRITICAL("Unable to find connection {}", logPtr(&conn));
170         return;
171     }
172     BMCWEB_LOG_DEBUG("Remove connection {} from obmc console", logPtr(&conn));
173 
174     // Removed last connection so remove the path
175     getConsoleHandlerMap().erase(iter);
176 }
177 
connectConsoleSocket(crow::websocket::Connection & conn,const boost::system::error_code & ec,const sdbusplus::message::unix_fd & unixfd)178 inline void connectConsoleSocket(crow::websocket::Connection& conn,
179                                  const boost::system::error_code& ec,
180                                  const sdbusplus::message::unix_fd& unixfd)
181 {
182     if (ec)
183     {
184         BMCWEB_LOG_ERROR(
185             "Failed to call console Connect() method DBUS error: {}",
186             ec.message());
187         conn.close("Failed to connect");
188         return;
189     }
190 
191     // Look up the handler
192     auto iter = getConsoleHandlerMap().find(&conn);
193     if (iter == getConsoleHandlerMap().end())
194     {
195         BMCWEB_LOG_ERROR("Connection was already closed");
196         return;
197     }
198 
199     int fd = dup(unixfd);
200     if (fd == -1)
201     {
202         BMCWEB_LOG_ERROR("Failed to dup the DBUS unixfd error: {}",
203                          strerror(errno));
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 
217 inline void
processConsoleObject(crow::websocket::Connection & conn,const std::string & consoleObjPath,const boost::system::error_code & ec,const::dbus::utility::MapperGetObject & objInfo)218     processConsoleObject(crow::websocket::Connection& conn,
219                          const std::string& consoleObjPath,
220                          const boost::system::error_code& ec,
221                          const ::dbus::utility::MapperGetObject& objInfo)
222 {
223     // Look up the handler
224     auto iter = getConsoleHandlerMap().find(&conn);
225     if (iter == getConsoleHandlerMap().end())
226     {
227         BMCWEB_LOG_ERROR("Connection was already closed");
228         return;
229     }
230 
231     if (ec)
232     {
233         BMCWEB_LOG_WARNING("getDbusObject() for consoles failed. DBUS error:{}",
234                            ec.message());
235         conn.close("getDbusObject() for consoles failed.");
236         return;
237     }
238 
239     const auto valueIface = objInfo.begin();
240     if (valueIface == objInfo.end())
241     {
242         BMCWEB_LOG_WARNING("getDbusObject() returned unexpected size: {}",
243                            objInfo.size());
244         conn.close("getDbusObject() returned unexpected size");
245         return;
246     }
247 
248     const std::string& consoleService = valueIface->first;
249     BMCWEB_LOG_DEBUG("Looking up unixFD for Service {} Path {}", consoleService,
250                      consoleObjPath);
251     // Call Connect() method to get the unix FD
252     crow::connections::systemBus->async_method_call(
253         [&conn](const boost::system::error_code& ec1,
254                 const sdbusplus::message::unix_fd& unixfd) {
255         connectConsoleSocket(conn, ec1, unixfd);
256     },
257         consoleService, consoleObjPath, "xyz.openbmc_project.Console.Access",
258         "Connect");
259 }
260 
261 // Query consoles from DBUS and find the matching to the
262 // rules string.
onOpen(crow::websocket::Connection & conn)263 inline void onOpen(crow::websocket::Connection& conn)
264 {
265     std::string consoleLeaf;
266 
267     BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn));
268 
269     if (getConsoleHandlerMap().size() >= maxSessions)
270     {
271         conn.close("Max sessions are already connected");
272         return;
273     }
274 
275     std::shared_ptr<ConsoleHandler> handler =
276         std::make_shared<ConsoleHandler>(conn.getIoContext(), conn);
277     getConsoleHandlerMap().emplace(&conn, handler);
278 
279     conn.deferRead();
280 
281     // Keep old path for backward compatibility
282     if (conn.url().path() == "/console0")
283     {
284         consoleLeaf = "default";
285     }
286     else
287     {
288         // Get the console id from console router path and prepare the console
289         // object path and console service.
290         consoleLeaf = conn.url().segments().back();
291     }
292     std::string consolePath =
293         sdbusplus::message::object_path("/xyz/openbmc_project/console") /
294         consoleLeaf;
295 
296     BMCWEB_LOG_DEBUG("Console Object path = {} Request target = {}",
297                      consolePath, conn.url().path());
298 
299     // mapper call lambda
300     constexpr std::array<std::string_view, 1> interfaces = {
301         "xyz.openbmc_project.Console.Access"};
302 
303     dbus::utility::getDbusObject(
304         consolePath, interfaces,
305         [&conn, consolePath](const boost::system::error_code& ec,
306                              const ::dbus::utility::MapperGetObject& objInfo) {
307         processConsoleObject(conn, consolePath, ec, objInfo);
308     });
309 }
310 
onMessage(crow::websocket::Connection & conn,const std::string & data,bool)311 inline void onMessage(crow::websocket::Connection& conn,
312                       const std::string& data, bool /*isBinary*/)
313 {
314     auto handler = getConsoleHandlerMap().find(&conn);
315     if (handler == getConsoleHandlerMap().end())
316     {
317         BMCWEB_LOG_CRITICAL("Unable to find connection {}", logPtr(&conn));
318         return;
319     }
320     handler->second->inputBuffer += data;
321     handler->second->doWrite();
322 }
323 
requestRoutes(App & app)324 inline void requestRoutes(App& app)
325 {
326     BMCWEB_ROUTE(app, "/console0")
327         .privileges({{"OpenBMCHostConsole"}})
328         .websocket()
329         .onopen(onOpen)
330         .onclose(onClose)
331         .onmessage(onMessage);
332 
333     BMCWEB_ROUTE(app, "/console/<str>")
334         .privileges({{"OpenBMCHostConsole"}})
335         .websocket()
336         .onopen(onOpen)
337         .onclose(onClose)
338         .onmessage(onMessage);
339 }
340 } // namespace obmc_console
341 } // namespace crow
342