xref: /openbmc/bmcweb/include/obmc_console.hpp (revision 1b8b02a4)
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 {}", ec.message());
71                 return;
72             }
73             self->doWrite();
74             });
75     }
76 
77     static void afterSendEx(const std::weak_ptr<ConsoleHandler>& weak)
78     {
79         std::shared_ptr<ConsoleHandler> self = weak.lock();
80         if (self == nullptr)
81         {
82             return;
83         }
84         self->doRead();
85     }
86 
87     void doRead()
88     {
89         BMCWEB_LOG_DEBUG("Reading from socket");
90         hostSocket.async_read_some(
91             boost::asio::buffer(outputBuffer),
92             [this, weakSelf(weak_from_this())](
93                 const boost::system::error_code& ec, std::size_t bytesRead) {
94             BMCWEB_LOG_DEBUG("read done.  Read {} bytes", bytesRead);
95             std::shared_ptr<ConsoleHandler> self = weakSelf.lock();
96             if (self == nullptr)
97             {
98                 return;
99             }
100             if (ec)
101             {
102                 BMCWEB_LOG_ERROR("Couldn't read from host serial port: {}",
103                                  ec.message());
104                 conn.close("Error connecting to host port");
105                 return;
106             }
107             std::string_view payload(outputBuffer.data(), bytesRead);
108             self->conn.sendEx(crow::websocket::MessageType::Binary, payload,
109                               std::bind_front(afterSendEx, weak_from_this()));
110             });
111     }
112 
113     bool connect(int fd)
114     {
115         boost::system::error_code ec;
116         boost::asio::local::stream_protocol proto;
117 
118         hostSocket.assign(proto, fd, ec);
119 
120         if (ec)
121         {
122             BMCWEB_LOG_ERROR(
123                 "Failed to assign the DBUS socket Socket assign error: {}",
124                 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 {}", logPtr(&conn));
164         return;
165     }
166     BMCWEB_LOG_DEBUG("Remove connection {} from obmc console", logPtr(&conn));
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(
179             "Failed to call console Connect() method DBUS error: {}",
180             ec.message());
181         conn.close("Failed to connect");
182         return;
183     }
184 
185     // Look up the handler
186     auto iter = getConsoleHandlerMap().find(&conn);
187     if (iter == getConsoleHandlerMap().end())
188     {
189         BMCWEB_LOG_ERROR("Connection was already closed");
190         return;
191     }
192 
193     int fd = dup(unixfd);
194     if (fd == -1)
195     {
196         BMCWEB_LOG_ERROR("Failed to dup the DBUS unixfd error: {}",
197                          strerror(errno));
198         conn.close("Internal error");
199         return;
200     }
201 
202     BMCWEB_LOG_DEBUG("Console duped FD: {}", fd);
203 
204     if (!iter->second->connect(fd))
205     {
206         close(fd);
207         conn.close("Internal Error");
208     }
209 }
210 
211 inline void
212     processConsoleObject(crow::websocket::Connection& conn,
213                          const std::string& consoleObjPath,
214                          const boost::system::error_code& ec,
215                          const ::dbus::utility::MapperGetObject& objInfo)
216 {
217     // Look up the handler
218     auto iter = getConsoleHandlerMap().find(&conn);
219     if (iter == getConsoleHandlerMap().end())
220     {
221         BMCWEB_LOG_ERROR("Connection was already closed");
222         return;
223     }
224 
225     if (ec)
226     {
227         BMCWEB_LOG_WARNING("getDbusObject() for consoles failed. DBUS error:{}",
228                            ec.message());
229         conn.close("getDbusObject() for consoles failed.");
230         return;
231     }
232 
233     const auto valueIface = objInfo.begin();
234     if (valueIface == objInfo.end())
235     {
236         BMCWEB_LOG_WARNING("getDbusObject() returned unexpected size: {}",
237                            objInfo.size());
238         conn.close("getDbusObject() returned unexpected size");
239         return;
240     }
241 
242     const std::string& consoleService = valueIface->first;
243     BMCWEB_LOG_DEBUG("Looking up unixFD for Service {} Path {}", consoleService,
244                      consoleObjPath);
245     // Call Connect() method to get the unix FD
246     crow::connections::systemBus->async_method_call(
247         [&conn](const boost::system::error_code& ec1,
248                 const sdbusplus::message::unix_fd& unixfd) {
249         connectConsoleSocket(conn, ec1, unixfd);
250         },
251         consoleService, consoleObjPath, "xyz.openbmc_project.Console.Access",
252         "Connect");
253 }
254 
255 // Query consoles from DBUS and find the matching to the
256 // rules string.
257 inline void onOpen(crow::websocket::Connection& conn)
258 {
259     std::string consoleLeaf;
260 
261     BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn));
262 
263     if (getConsoleHandlerMap().size() >= maxSessions)
264     {
265         conn.close("Max sessions are already connected");
266         return;
267     }
268 
269     std::shared_ptr<ConsoleHandler> handler =
270         std::make_shared<ConsoleHandler>(conn.getIoContext(), conn);
271     getConsoleHandlerMap().emplace(&conn, handler);
272 
273     conn.deferRead();
274 
275     // Keep old path for backward compatibility
276     if (conn.url().path() == "/console0")
277     {
278         consoleLeaf = "default";
279     }
280     else
281     {
282         // Get the console id from console router path and prepare the console
283         // object path and console service.
284         consoleLeaf = conn.url().segments().back();
285     }
286     std::string consolePath =
287         sdbusplus::message::object_path("/xyz/openbmc_project/console") /
288         consoleLeaf;
289 
290     BMCWEB_LOG_DEBUG("Console Object path = {} Request target = {}",
291                      consolePath, conn.url().path());
292 
293     // mapper call lambda
294     constexpr std::array<std::string_view, 1> interfaces = {
295         "xyz.openbmc_project.Console.Access"};
296 
297     dbus::utility::getDbusObject(
298         consolePath, interfaces,
299         [&conn, consolePath](const boost::system::error_code& ec,
300                              const ::dbus::utility::MapperGetObject& objInfo) {
301         processConsoleObject(conn, consolePath, ec, objInfo);
302         });
303 }
304 
305 inline void onMessage(crow::websocket::Connection& conn,
306                       const std::string& data, bool /*isBinary*/)
307 {
308     auto handler = getConsoleHandlerMap().find(&conn);
309     if (handler == getConsoleHandlerMap().end())
310     {
311         BMCWEB_LOG_CRITICAL("Unable to find connection {}", logPtr(&conn));
312         return;
313     }
314     handler->second->inputBuffer += data;
315     handler->second->doWrite();
316 }
317 
318 inline void requestRoutes(App& app)
319 {
320     BMCWEB_ROUTE(app, "/console0")
321         .privileges({{"OpenBMCHostConsole"}})
322         .websocket()
323         .onopen(onOpen)
324         .onclose(onClose)
325         .onmessage(onMessage);
326 
327     BMCWEB_ROUTE(app, "/console/<str>")
328         .privileges({{"OpenBMCHostConsole"}})
329         .websocket()
330         .onopen(onOpen)
331         .onclose(onClose)
332         .onmessage(onMessage);
333 }
334 } // namespace obmc_console
335 } // namespace crow
336