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 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 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