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