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