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