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