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