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