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 " << connection; 48 return 0; 49 } 50 sdbusplus::message_t message(m); 51 nlohmann::json json; 52 json["event"] = message.get_member(); 53 json["path"] = message.get_path(); 54 if (strcmp(message.get_member(), "PropertiesChanged") == 0) 55 { 56 nlohmann::json data; 57 int r = openbmc_mapper::convertDBusToJSON("sa{sv}as", message, data); 58 if (r < 0) 59 { 60 BMCWEB_LOG_ERROR << "convertDBusToJSON failed with " << r; 61 return 0; 62 } 63 if (!data.is_array()) 64 { 65 BMCWEB_LOG_ERROR << "No data in PropertiesChanged signal"; 66 return 0; 67 } 68 69 // data is type sa{sv}as and is an array[3] of string, object, array 70 json["interface"] = data[0]; 71 json["properties"] = data[1]; 72 } 73 else if (strcmp(message.get_member(), "InterfacesAdded") == 0) 74 { 75 nlohmann::json data; 76 int r = openbmc_mapper::convertDBusToJSON("oa{sa{sv}}", message, data); 77 if (r < 0) 78 { 79 BMCWEB_LOG_ERROR << "convertDBusToJSON failed with " << r; 80 return 0; 81 } 82 83 if (!data.is_array()) 84 { 85 BMCWEB_LOG_ERROR << "No data in InterfacesAdded signal"; 86 return 0; 87 } 88 89 // data is type oa{sa{sv}} which is an array[2] of string, object 90 for (const auto& entry : data[1].items()) 91 { 92 auto it = thisSession->second.interfaces.find(entry.key()); 93 if (it != thisSession->second.interfaces.end()) 94 { 95 json["interfaces"][entry.key()] = entry.value(); 96 } 97 } 98 } 99 else 100 { 101 BMCWEB_LOG_CRITICAL << "message " << message.get_member() 102 << " was unexpected"; 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 " << &conn << " opened"; 118 sessions.try_emplace(&conn); 119 }) 120 .onclose([&](crow::websocket::Connection& conn, const std::string&) { 121 sessions.erase(&conn); 122 }) 123 .onmessage( 124 [&](crow::websocket::Connection& conn, const std::string& data, 125 bool) { 126 const auto sessionPair = sessions.find(&conn); 127 if (sessionPair == sessions.end()) 128 { 129 conn.close("Internal error"); 130 } 131 DbusWebsocketSession& thisSession = sessionPair->second; 132 BMCWEB_LOG_DEBUG << "Connection " << &conn << " received " << data; 133 nlohmann::json j = nlohmann::json::parse(data, nullptr, false); 134 if (j.is_discarded()) 135 { 136 BMCWEB_LOG_ERROR << "Unable to parse json data for monitor"; 137 conn.close("Unable to parse json request"); 138 return; 139 } 140 nlohmann::json::iterator interfaces = j.find("interfaces"); 141 if (interfaces != j.end()) 142 { 143 thisSession.interfaces.reserve(interfaces->size()); 144 for (auto& interface : *interfaces) 145 { 146 const std::string* str = 147 interface.get_ptr<const std::string*>(); 148 if (str != nullptr) 149 { 150 thisSession.interfaces.insert(*str); 151 } 152 } 153 } 154 155 nlohmann::json::iterator paths = j.find("paths"); 156 if (paths == j.end()) 157 { 158 BMCWEB_LOG_ERROR << "Unable to find paths in json data"; 159 conn.close("Unable to find paths in json data"); 160 return; 161 } 162 163 size_t interfaceCount = thisSession.interfaces.size(); 164 if (interfaceCount == 0) 165 { 166 interfaceCount = 1; 167 } 168 // Reserve our matches upfront. For each path there is 1 for 169 // interfacesAdded, and InterfaceCount number for 170 // PropertiesChanged 171 thisSession.matches.reserve(thisSession.matches.size() + 172 paths->size() * (1U + interfaceCount)); 173 174 // These regexes derived on the rules here: 175 // https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names 176 static std::regex validPath("^/([A-Za-z0-9_]+/?)*$"); 177 static std::regex validInterface( 178 "^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)+$"); 179 180 for (const auto& thisPath : *paths) 181 { 182 const std::string* thisPathString = 183 thisPath.get_ptr<const std::string*>(); 184 if (thisPathString == nullptr) 185 { 186 BMCWEB_LOG_ERROR << "subscribe path isn't a string?"; 187 conn.close(); 188 return; 189 } 190 if (!std::regex_match(*thisPathString, validPath)) 191 { 192 BMCWEB_LOG_ERROR << "Invalid path name " << *thisPathString; 193 conn.close(); 194 return; 195 } 196 std::string propertiesMatchString = 197 ("type='signal'," 198 "interface='org.freedesktop.DBus.Properties'," 199 "path_namespace='" + 200 *thisPathString + 201 "'," 202 "member='PropertiesChanged'"); 203 // If interfaces weren't specified, add a single match for all 204 // interfaces 205 if (thisSession.interfaces.empty()) 206 { 207 BMCWEB_LOG_DEBUG << "Creating match " << propertiesMatchString; 208 209 thisSession.matches.emplace_back( 210 std::make_unique<sdbusplus::bus::match_t>( 211 *crow::connections::systemBus, propertiesMatchString, 212 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