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