xref: /openbmc/bmcweb/include/dbus_monitor.hpp (revision 40e9b92ec19acffb46f83a6e55b18974da5d708e)
1  // SPDX-License-Identifier: Apache-2.0
2  // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3  #pragma once
4  #include "app.hpp"
5  #include "async_resp.hpp"
6  #include "dbus_singleton.hpp"
7  #include "openbmc_dbus_rest.hpp"
8  #include "websocket.hpp"
9  
10  #include <boost/container/flat_map.hpp>
11  #include <boost/container/flat_set.hpp>
12  #include <sdbusplus/bus/match.hpp>
13  #include <sdbusplus/message/types.hpp>
14  
15  #include <variant>
16  
17  namespace crow
18  {
19  namespace dbus_monitor
20  {
21  
22  struct DbusWebsocketSession
23  {
24      std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches;
25      boost::container::flat_set<std::string, std::less<>,
26                                 std::vector<std::string>>
27          interfaces;
28  };
29  
30  using SessionMap = boost::container::flat_map<crow::websocket::Connection*,
31                                                DbusWebsocketSession>;
32  
33  // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
34  static SessionMap sessions;
35  
36  inline int onPropertyUpdate(sd_bus_message* m, void* userdata,
37                              sd_bus_error* retError)
38  {
39      if (retError == nullptr || (sd_bus_error_is_set(retError) != 0))
40      {
41          BMCWEB_LOG_ERROR("Got sdbus error on match");
42          return 0;
43      }
44      crow::websocket::Connection* connection =
45          static_cast<crow::websocket::Connection*>(userdata);
46      auto thisSession = sessions.find(connection);
47      if (thisSession == sessions.end())
48      {
49          BMCWEB_LOG_ERROR("Couldn't find dbus connection {}",
50                           logPtr(connection));
51          return 0;
52      }
53      sdbusplus::message_t message(m);
54      nlohmann::json json;
55      json["event"] = message.get_member();
56      json["path"] = message.get_path();
57      if (strcmp(message.get_member(), "PropertiesChanged") == 0)
58      {
59          nlohmann::json data;
60          int r = openbmc_mapper::convertDBusToJSON("sa{sv}as", message, data);
61          if (r < 0)
62          {
63              BMCWEB_LOG_ERROR("convertDBusToJSON failed with {}", r);
64              return 0;
65          }
66          if (!data.is_array())
67          {
68              BMCWEB_LOG_ERROR("No data in PropertiesChanged signal");
69              return 0;
70          }
71  
72          // data is type sa{sv}as and is an array[3] of string, object, array
73          json["interface"] = data[0];
74          json["properties"] = data[1];
75      }
76      else if (strcmp(message.get_member(), "InterfacesAdded") == 0)
77      {
78          nlohmann::json data;
79          int r = openbmc_mapper::convertDBusToJSON("oa{sa{sv}}", message, data);
80          if (r < 0)
81          {
82              BMCWEB_LOG_ERROR("convertDBusToJSON failed with {}", r);
83              return 0;
84          }
85          nlohmann::json::array_t* arr = data.get_ptr<nlohmann::json::array_t*>();
86          if (arr == nullptr)
87          {
88              BMCWEB_LOG_ERROR("No data in InterfacesAdded signal");
89              return 0;
90          }
91          if (arr->size() < 2)
92          {
93              BMCWEB_LOG_ERROR("No data in InterfacesAdded signal");
94              return 0;
95          }
96  
97          nlohmann::json::object_t* obj =
98              (*arr)[1].get_ptr<nlohmann::json::object_t*>();
99          if (obj == nullptr)
100          {
101              BMCWEB_LOG_ERROR("No data in InterfacesAdded signal");
102              return 0;
103          }
104          // data is type oa{sa{sv}} which is an array[2] of string, object
105          for (const auto& entry : *obj)
106          {
107              auto it = thisSession->second.interfaces.find(entry.first);
108              if (it != thisSession->second.interfaces.end())
109              {
110                  json["interfaces"][entry.first] = entry.second;
111              }
112          }
113      }
114      else
115      {
116          BMCWEB_LOG_CRITICAL("message {} was unexpected", message.get_member());
117          return 0;
118      }
119  
120      connection->sendText(
121          json.dump(2, ' ', true, nlohmann::json::error_handler_t::replace));
122      return 0;
123  }
124  
125  inline void requestRoutes(App& app)
126  {
127      BMCWEB_ROUTE(app, "/subscribe")
128          .privileges({{"Login"}})
129          .websocket()
130          .onopen([](crow::websocket::Connection& conn) {
131              BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn));
132              sessions.try_emplace(&conn);
133          })
134          .onclose([](crow::websocket::Connection& conn, const std::string&) {
135              sessions.erase(&conn);
136          })
137          .onmessage([](crow::websocket::Connection& conn,
138                        const std::string& data, bool) {
139              const auto sessionPair = sessions.find(&conn);
140              if (sessionPair == sessions.end())
141              {
142                  conn.close("Internal error");
143              }
144              DbusWebsocketSession& thisSession = sessionPair->second;
145              BMCWEB_LOG_DEBUG("Connection {} received {}", logPtr(&conn), data);
146              nlohmann::json j = nlohmann::json::parse(data, nullptr, false);
147              if (j.is_discarded())
148              {
149                  BMCWEB_LOG_ERROR("Unable to parse json data for monitor");
150                  conn.close("Unable to parse json request");
151                  return;
152              }
153              nlohmann::json::iterator interfaces = j.find("interfaces");
154              if (interfaces != j.end())
155              {
156                  thisSession.interfaces.reserve(interfaces->size());
157                  for (auto& interface : *interfaces)
158                  {
159                      const std::string* str =
160                          interface.get_ptr<const std::string*>();
161                      if (str != nullptr)
162                      {
163                          thisSession.interfaces.insert(*str);
164                      }
165                  }
166              }
167  
168              nlohmann::json::iterator paths = j.find("paths");
169              if (paths == j.end())
170              {
171                  BMCWEB_LOG_ERROR("Unable to find paths in json data");
172                  conn.close("Unable to find paths in json data");
173                  return;
174              }
175  
176              size_t interfaceCount = thisSession.interfaces.size();
177              if (interfaceCount == 0)
178              {
179                  interfaceCount = 1;
180              }
181  
182              // These regexes derived on the rules here:
183              // https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names
184              static std::regex validPath("^/([A-Za-z0-9_]+/?)*$");
185              static std::regex validInterface(
186                  "^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)+$");
187  
188              for (const auto& thisPath : *paths)
189              {
190                  const std::string* thisPathString =
191                      thisPath.get_ptr<const std::string*>();
192                  if (thisPathString == nullptr)
193                  {
194                      BMCWEB_LOG_ERROR("subscribe path isn't a string?");
195                      conn.close();
196                      return;
197                  }
198                  if (!std::regex_match(*thisPathString, validPath))
199                  {
200                      BMCWEB_LOG_ERROR("Invalid path name {}", *thisPathString);
201                      conn.close();
202                      return;
203                  }
204                  std::string propertiesMatchString =
205                      ("type='signal',"
206                       "interface='org.freedesktop.DBus.Properties',"
207                       "path_namespace='" +
208                       *thisPathString +
209                       "',"
210                       "member='PropertiesChanged'");
211                  // If interfaces weren't specified, add a single match for all
212                  // interfaces
213                  if (thisSession.interfaces.empty())
214                  {
215                      BMCWEB_LOG_DEBUG("Creating match {}",
216                                       propertiesMatchString);
217  
218                      thisSession.matches.emplace_back(
219                          std::make_unique<sdbusplus::bus::match_t>(
220                              *crow::connections::systemBus,
221                              propertiesMatchString, onPropertyUpdate, &conn));
222                  }
223                  else
224                  {
225                      // If interfaces were specified, add a match for each
226                      // interface
227                      for (const std::string& interface : thisSession.interfaces)
228                      {
229                          if (!std::regex_match(interface, validInterface))
230                          {
231                              BMCWEB_LOG_ERROR("Invalid interface name {}",
232                                               interface);
233                              conn.close();
234                              return;
235                          }
236                          std::string ifaceMatchString = propertiesMatchString;
237                          ifaceMatchString += ",arg0='";
238                          ifaceMatchString += interface;
239                          ifaceMatchString += "'";
240                          BMCWEB_LOG_DEBUG("Creating match {}", ifaceMatchString);
241                          thisSession.matches.emplace_back(
242                              std::make_unique<sdbusplus::bus::match_t>(
243                                  *crow::connections::systemBus, ifaceMatchString,
244                                  onPropertyUpdate, &conn));
245                      }
246                  }
247                  std::string objectManagerMatchString =
248                      ("type='signal',"
249                       "interface='org.freedesktop.DBus.ObjectManager',"
250                       "path_namespace='" +
251                       *thisPathString +
252                       "',"
253                       "member='InterfacesAdded'");
254                  BMCWEB_LOG_DEBUG("Creating match {}", objectManagerMatchString);
255                  thisSession.matches.emplace_back(
256                      std::make_unique<sdbusplus::bus::match_t>(
257                          *crow::connections::systemBus, objectManagerMatchString,
258                          onPropertyUpdate, &conn));
259              }
260          });
261  }
262  } // namespace dbus_monitor
263  } // namespace crow
264