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