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