xref: /openbmc/bmcweb/include/obmc_console.hpp (revision 88ada3bc)
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 << "Failed to find the handler";
189         conn.close("Internal error");
190         return;
191     }
192 
193     int fd = dup(unixfd);
194     if (fd == -1)
195     {
196         BMCWEB_LOG_ERROR << "Failed to dup the DBUS unixfd"
197                          << " error: " << strerror(errno);
198         conn.close("Internal error");
199         return;
200     }
201 
202     BMCWEB_LOG_DEBUG << "Console web socket path: " << conn.req.target()
203                      << " Console unix FD: " << unixfd << " duped FD: " << fd;
204 
205     if (!iter->second->connect(fd))
206     {
207         close(fd);
208         conn.close("Internal Error");
209     }
210 }
211 
212 // Query consoles from DBUS and find the matching to the
213 // rules string.
214 inline void onOpen(crow::websocket::Connection& conn)
215 {
216     BMCWEB_LOG_DEBUG << "Connection " << &conn << " opened";
217 
218     if (getConsoleHandlerMap().size() >= maxSessions)
219     {
220         conn.close("Max sessions are already connected");
221         return;
222     }
223 
224     std::shared_ptr<ConsoleHandler> handler =
225         std::make_shared<ConsoleHandler>(conn.getIoContext(), conn);
226     getConsoleHandlerMap().emplace(&conn, handler);
227 
228     conn.deferRead();
229 
230     // The console id 'default' is used for the console0
231     // We need to change it when we provide full multi-console support.
232     const std::string consolePath = "/xyz/openbmc_project/console/default";
233     const std::string consoleService = "xyz.openbmc_project.Console.default";
234 
235     BMCWEB_LOG_DEBUG << "Console Object path = " << consolePath
236                      << " service = " << consoleService
237                      << " Request target = " << conn.req.target();
238 
239     // Call Connect() method to get the unix FD
240     crow::connections::systemBus->async_method_call(
241         [&conn](const boost::system::error_code& ec,
242                 const sdbusplus::message::unix_fd& unixfd) {
243         connectConsoleSocket(conn, ec, unixfd);
244         },
245         consoleService, consolePath, "xyz.openbmc_project.Console.Access",
246         "Connect");
247 }
248 
249 inline void onMessage(crow::websocket::Connection& conn,
250                       const std::string& data, bool /*isBinary*/)
251 {
252     auto handler = getConsoleHandlerMap().find(&conn);
253     if (handler == getConsoleHandlerMap().end())
254     {
255         BMCWEB_LOG_CRITICAL << "Unable to find connection " << &conn;
256         return;
257     }
258     handler->second->inputBuffer += data;
259     handler->second->doWrite();
260 }
261 
262 inline void requestRoutes(App& app)
263 {
264     BMCWEB_ROUTE(app, "/console0")
265         .privileges({{"OpenBMCHostConsole"}})
266         .websocket()
267         .onopen(onOpen)
268         .onclose(onClose)
269         .onmessage(onMessage);
270 }
271 } // namespace obmc_console
272 } // namespace crow
273