xref: /openbmc/bmcweb/include/kvm_websocket.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 "io_context_singleton.hpp"
6 #include "logging.hpp"
7 #include "websocket.hpp"
8 
9 #include <sys/types.h>
10 
11 #include <boost/asio/buffer.hpp>
12 #include <boost/asio/error.hpp>
13 #include <boost/asio/ip/address.hpp>
14 #include <boost/asio/ip/tcp.hpp>
15 #include <boost/beast/core/flat_static_buffer.hpp>
16 #include <boost/container/flat_map.hpp>
17 
18 #include <cstddef>
19 #include <memory>
20 #include <string>
21 
22 namespace crow
23 {
24 namespace obmc_kvm
25 {
26 
27 static constexpr const uint maxSessions = 4;
28 
29 class KvmSession : public std::enable_shared_from_this<KvmSession>
30 {
31   public:
KvmSession(crow::websocket::Connection & connIn)32     explicit KvmSession(crow::websocket::Connection& connIn) :
33         conn(connIn), hostSocket(getIoContext())
34     {
35         boost::asio::ip::tcp::endpoint endpoint(
36             boost::asio::ip::make_address("127.0.0.1"), 5900);
37         hostSocket.async_connect(
38             endpoint, [this, &connIn](const boost::system::error_code& ec) {
39                 if (ec)
40                 {
41                     BMCWEB_LOG_ERROR(
42                         "conn:{}, Couldn't connect to KVM socket port: {}",
43                         logPtr(&conn), ec);
44                     if (ec != boost::asio::error::operation_aborted)
45                     {
46                         connIn.close("Error in connecting to KVM port");
47                     }
48                     return;
49                 }
50 
51                 doRead();
52             });
53     }
54 
onMessage(const std::string & data)55     void onMessage(const std::string& data)
56     {
57         if (data.length() > inputBuffer.capacity())
58         {
59             BMCWEB_LOG_ERROR("conn:{}, Buffer overrun when writing {} bytes",
60                              logPtr(&conn), data.length());
61             conn.close("Buffer overrun");
62             return;
63         }
64 
65         BMCWEB_LOG_DEBUG("conn:{}, Read {} bytes from websocket", logPtr(&conn),
66                          data.size());
67         size_t copied = boost::asio::buffer_copy(
68             inputBuffer.prepare(data.size()), boost::asio::buffer(data));
69         BMCWEB_LOG_DEBUG("conn:{}, Committing {} bytes from websocket",
70                          logPtr(&conn), copied);
71         inputBuffer.commit(copied);
72 
73         BMCWEB_LOG_DEBUG("conn:{}, inputbuffer size {}", logPtr(&conn),
74                          inputBuffer.size());
75         doWrite();
76     }
77 
78   protected:
doRead()79     void doRead()
80     {
81         std::size_t bytes = outputBuffer.capacity() - outputBuffer.size();
82         BMCWEB_LOG_DEBUG("conn:{}, Reading {} from kvm socket", logPtr(&conn),
83                          bytes);
84         hostSocket.async_read_some(
85             outputBuffer.prepare(outputBuffer.capacity() - outputBuffer.size()),
86             [this, weak(weak_from_this())](const boost::system::error_code& ec,
87                                            std::size_t bytesRead) {
88                 auto self = weak.lock();
89                 if (self == nullptr)
90                 {
91                     return;
92                 }
93                 BMCWEB_LOG_DEBUG("conn:{}, read done.  Read {} bytes",
94                                  logPtr(&conn), bytesRead);
95                 if (ec)
96                 {
97                     BMCWEB_LOG_ERROR(
98                         "conn:{}, Couldn't read from KVM socket port: {}",
99                         logPtr(&conn), ec);
100                     if (ec != boost::asio::error::operation_aborted)
101                     {
102                         conn.close("Error in connecting to KVM port");
103                     }
104                     return;
105                 }
106 
107                 outputBuffer.commit(bytesRead);
108                 std::string_view payload(
109                     static_cast<const char*>(outputBuffer.data().data()),
110                     bytesRead);
111                 BMCWEB_LOG_DEBUG("conn:{}, Sending payload size {}",
112                                  logPtr(&conn), payload.size());
113                 conn.sendBinary(payload);
114                 outputBuffer.consume(bytesRead);
115 
116                 doRead();
117             });
118     }
119 
doWrite()120     void doWrite()
121     {
122         if (doingWrite)
123         {
124             BMCWEB_LOG_DEBUG("conn:{}, Already writing.  Bailing out",
125                              logPtr(&conn));
126             return;
127         }
128         if (inputBuffer.size() == 0)
129         {
130             BMCWEB_LOG_DEBUG("conn:{}, inputBuffer empty.  Bailing out",
131                              logPtr(&conn));
132             return;
133         }
134 
135         doingWrite = true;
136         hostSocket.async_write_some(
137             inputBuffer.data(),
138             [this, weak(weak_from_this())](const boost::system::error_code& ec,
139                                            std::size_t bytesWritten) {
140                 auto self = weak.lock();
141                 if (self == nullptr)
142                 {
143                     return;
144                 }
145                 BMCWEB_LOG_DEBUG("conn:{}, Wrote {}bytes", logPtr(&conn),
146                                  bytesWritten);
147                 doingWrite = false;
148                 inputBuffer.consume(bytesWritten);
149 
150                 if (ec == boost::asio::error::eof)
151                 {
152                     conn.close("KVM socket port closed");
153                     return;
154                 }
155                 if (ec)
156                 {
157                     BMCWEB_LOG_ERROR("conn:{}, Error in KVM socket write {}",
158                                      logPtr(&conn), ec);
159                     if (ec != boost::asio::error::operation_aborted)
160                     {
161                         conn.close("Error in reading to host port");
162                     }
163                     return;
164                 }
165 
166                 doWrite();
167             });
168     }
169 
170     crow::websocket::Connection& conn;
171     boost::asio::ip::tcp::socket hostSocket;
172     boost::beast::flat_static_buffer<1024UL * 50UL> outputBuffer;
173     boost::beast::flat_static_buffer<1024UL> inputBuffer;
174     bool doingWrite{false};
175 };
176 
177 using SessionMap = boost::container::flat_map<crow::websocket::Connection*,
178                                               std::shared_ptr<KvmSession>>;
179 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
180 static SessionMap sessions;
181 
requestRoutes(App & app)182 inline void requestRoutes(App& app)
183 {
184     sessions.reserve(maxSessions);
185 
186     BMCWEB_ROUTE(app, "/kvm/0")
187         .privileges({{"ConfigureComponents", "ConfigureManager"}})
188         .websocket()
189         .onopen([](crow::websocket::Connection& conn) {
190             BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn));
191 
192             if (sessions.size() == maxSessions)
193             {
194                 conn.close("Max sessions are already connected");
195                 return;
196             }
197 
198             sessions[&conn] = std::make_shared<KvmSession>(conn);
199         })
200         .onclose([](crow::websocket::Connection& conn, const std::string&) {
201             sessions.erase(&conn);
202         })
203         .onmessage([](crow::websocket::Connection& conn,
204                       const std::string& data, bool) {
205             if (sessions[&conn])
206             {
207                 sessions[&conn]->onMessage(data);
208             }
209         });
210 }
211 
212 } // namespace obmc_kvm
213 } // namespace crow
214