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