140e9b92eSEd Tanous // SPDX-License-Identifier: Apache-2.0
240e9b92eSEd Tanous // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3911ac317SEd Tanous #pragma once
43ccb3adbSEd Tanous #include "app.hpp"
53ccb3adbSEd Tanous #include "dbus_singleton.hpp"
6*d7857201SEd Tanous #include "logging.hpp"
73ccb3adbSEd Tanous #include "openbmc_dbus_rest.hpp"
8faf100f9SEd Tanous #include "websocket.hpp"
93ccb3adbSEd Tanous
10*d7857201SEd Tanous #include <systemd/sd-bus.h>
11*d7857201SEd Tanous
12911ac317SEd Tanous #include <boost/container/flat_map.hpp>
139b243a4eSEd Tanous #include <boost/container/flat_set.hpp>
14*d7857201SEd Tanous #include <nlohmann/json.hpp>
151abe55efSEd Tanous #include <sdbusplus/bus/match.hpp>
16*d7857201SEd Tanous #include <sdbusplus/message.hpp>
171214b7e7SGunnar Mills
18*d7857201SEd Tanous #include <cstddef>
19*d7857201SEd Tanous #include <cstring>
20*d7857201SEd Tanous #include <functional>
21*d7857201SEd Tanous #include <memory>
22*d7857201SEd Tanous #include <regex>
23*d7857201SEd Tanous #include <string>
24*d7857201SEd Tanous #include <vector>
259b243a4eSEd Tanous
261abe55efSEd Tanous namespace crow
271abe55efSEd Tanous {
281abe55efSEd Tanous namespace dbus_monitor
291abe55efSEd Tanous {
30911ac317SEd Tanous
311abe55efSEd Tanous struct DbusWebsocketSession
321abe55efSEd Tanous {
3359d494eeSPatrick Williams std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches;
3424b2fe81SEd Tanous boost::container::flat_set<std::string, std::less<>,
3524b2fe81SEd Tanous std::vector<std::string>>
3624b2fe81SEd Tanous interfaces;
37911ac317SEd Tanous };
38911ac317SEd Tanous
39cf9e417dSEd Tanous using SessionMap = boost::container::flat_map<crow::websocket::Connection*,
40cf9e417dSEd Tanous DbusWebsocketSession>;
41cf9e417dSEd Tanous
42cf9e417dSEd Tanous // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
43cf9e417dSEd Tanous static SessionMap sessions;
44911ac317SEd Tanous
onPropertyUpdate(sd_bus_message * m,void * userdata,sd_bus_error * retError)459b243a4eSEd Tanous inline int onPropertyUpdate(sd_bus_message* m, void* userdata,
4681ce609eSEd Tanous sd_bus_error* retError)
471abe55efSEd Tanous {
48e662eae8SEd Tanous if (retError == nullptr || (sd_bus_error_is_set(retError) != 0))
491abe55efSEd Tanous {
5062598e31SEd Tanous BMCWEB_LOG_ERROR("Got sdbus error on match");
519b243a4eSEd Tanous return 0;
529b243a4eSEd Tanous }
539b243a4eSEd Tanous crow::websocket::Connection* connection =
549b243a4eSEd Tanous static_cast<crow::websocket::Connection*>(userdata);
559b243a4eSEd Tanous auto thisSession = sessions.find(connection);
561abe55efSEd Tanous if (thisSession == sessions.end())
571abe55efSEd Tanous {
5862598e31SEd Tanous BMCWEB_LOG_ERROR("Couldn't find dbus connection {}",
5962598e31SEd Tanous logPtr(connection));
60aa2e59c1SEd Tanous return 0;
61aa2e59c1SEd Tanous }
6259d494eeSPatrick Williams sdbusplus::message_t message(m);
631476687dSEd Tanous nlohmann::json json;
641476687dSEd Tanous json["event"] = message.get_member();
651476687dSEd Tanous json["path"] = message.get_path();
661abe55efSEd Tanous if (strcmp(message.get_member(), "PropertiesChanged") == 0)
671abe55efSEd Tanous {
68715748a1SMatt Spinler nlohmann::json data;
69715748a1SMatt Spinler int r = openbmc_mapper::convertDBusToJSON("sa{sv}as", message, data);
70715748a1SMatt Spinler if (r < 0)
71715748a1SMatt Spinler {
7262598e31SEd Tanous BMCWEB_LOG_ERROR("convertDBusToJSON failed with {}", r);
73715748a1SMatt Spinler return 0;
74715748a1SMatt Spinler }
75715748a1SMatt Spinler if (!data.is_array())
76715748a1SMatt Spinler {
7762598e31SEd Tanous BMCWEB_LOG_ERROR("No data in PropertiesChanged signal");
78715748a1SMatt Spinler return 0;
79715748a1SMatt Spinler }
80715748a1SMatt Spinler
81715748a1SMatt Spinler // data is type sa{sv}as and is an array[3] of string, object, array
821476687dSEd Tanous json["interface"] = data[0];
831476687dSEd Tanous json["properties"] = data[1];
841abe55efSEd Tanous }
851abe55efSEd Tanous else if (strcmp(message.get_member(), "InterfacesAdded") == 0)
861abe55efSEd Tanous {
87715748a1SMatt Spinler nlohmann::json data;
88715748a1SMatt Spinler int r = openbmc_mapper::convertDBusToJSON("oa{sa{sv}}", message, data);
89715748a1SMatt Spinler if (r < 0)
901abe55efSEd Tanous {
9162598e31SEd Tanous BMCWEB_LOG_ERROR("convertDBusToJSON failed with {}", r);
92715748a1SMatt Spinler return 0;
93715748a1SMatt Spinler }
940bdda665SEd Tanous nlohmann::json::array_t* arr = data.get_ptr<nlohmann::json::array_t*>();
950bdda665SEd Tanous if (arr == nullptr)
960bdda665SEd Tanous {
970bdda665SEd Tanous BMCWEB_LOG_ERROR("No data in InterfacesAdded signal");
980bdda665SEd Tanous return 0;
990bdda665SEd Tanous }
1000bdda665SEd Tanous if (arr->size() < 2)
101715748a1SMatt Spinler {
10262598e31SEd Tanous BMCWEB_LOG_ERROR("No data in InterfacesAdded signal");
103715748a1SMatt Spinler return 0;
104715748a1SMatt Spinler }
105715748a1SMatt Spinler
1060bdda665SEd Tanous nlohmann::json::object_t* obj =
1070bdda665SEd Tanous (*arr)[1].get_ptr<nlohmann::json::object_t*>();
1080bdda665SEd Tanous if (obj == nullptr)
109715748a1SMatt Spinler {
1100bdda665SEd Tanous BMCWEB_LOG_ERROR("No data in InterfacesAdded signal");
1110bdda665SEd Tanous return 0;
1120bdda665SEd Tanous }
1130bdda665SEd Tanous // data is type oa{sa{sv}} which is an array[2] of string, object
1140bdda665SEd Tanous for (const auto& entry : *obj)
1150bdda665SEd Tanous {
1160bdda665SEd Tanous auto it = thisSession->second.interfaces.find(entry.first);
1171abe55efSEd Tanous if (it != thisSession->second.interfaces.end())
1181abe55efSEd Tanous {
1190bdda665SEd Tanous json["interfaces"][entry.first] = entry.second;
120911ac317SEd Tanous }
1219b243a4eSEd Tanous }
1221abe55efSEd Tanous }
1231abe55efSEd Tanous else
1241abe55efSEd Tanous {
12562598e31SEd Tanous BMCWEB_LOG_CRITICAL("message {} was unexpected", message.get_member());
1269b243a4eSEd Tanous return 0;
1279b243a4eSEd Tanous }
1289b243a4eSEd Tanous
12971f52d96SEd Tanous connection->sendText(
1301476687dSEd Tanous json.dump(2, ' ', true, nlohmann::json::error_handler_t::replace));
1319b243a4eSEd Tanous return 0;
132271584abSEd Tanous }
133911ac317SEd Tanous
requestRoutes(App & app)13423a21a1cSEd Tanous inline void requestRoutes(App& app)
1351abe55efSEd Tanous {
1369b243a4eSEd Tanous BMCWEB_ROUTE(app, "/subscribe")
137432a890cSEd Tanous .privileges({{"Login"}})
138911ac317SEd Tanous .websocket()
13925ce6206SEd Tanous .onopen([](crow::websocket::Connection& conn) {
14062598e31SEd Tanous BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn));
14124b2fe81SEd Tanous sessions.try_emplace(&conn);
142911ac317SEd Tanous })
14325ce6206SEd Tanous .onclose([](crow::websocket::Connection& conn, const std::string&) {
144cb13a392SEd Tanous sessions.erase(&conn);
145cb13a392SEd Tanous })
14625ce6206SEd Tanous .onmessage([](crow::websocket::Connection& conn,
14762598e31SEd Tanous const std::string& data, bool) {
14824b2fe81SEd Tanous const auto sessionPair = sessions.find(&conn);
14924b2fe81SEd Tanous if (sessionPair == sessions.end())
15024b2fe81SEd Tanous {
15124b2fe81SEd Tanous conn.close("Internal error");
15224b2fe81SEd Tanous }
15324b2fe81SEd Tanous DbusWebsocketSession& thisSession = sessionPair->second;
15462598e31SEd Tanous BMCWEB_LOG_DEBUG("Connection {} received {}", logPtr(&conn), data);
1559b243a4eSEd Tanous nlohmann::json j = nlohmann::json::parse(data, nullptr, false);
1561abe55efSEd Tanous if (j.is_discarded())
1571abe55efSEd Tanous {
15862598e31SEd Tanous BMCWEB_LOG_ERROR("Unable to parse json data for monitor");
1599b243a4eSEd Tanous conn.close("Unable to parse json request");
1609b243a4eSEd Tanous return;
1619b243a4eSEd Tanous }
1629b243a4eSEd Tanous nlohmann::json::iterator interfaces = j.find("interfaces");
1631abe55efSEd Tanous if (interfaces != j.end())
1641abe55efSEd Tanous {
1659b243a4eSEd Tanous thisSession.interfaces.reserve(interfaces->size());
1661abe55efSEd Tanous for (auto& interface : *interfaces)
1671abe55efSEd Tanous {
1681abe55efSEd Tanous const std::string* str =
1691abe55efSEd Tanous interface.get_ptr<const std::string*>();
1701abe55efSEd Tanous if (str != nullptr)
1711abe55efSEd Tanous {
1729b243a4eSEd Tanous thisSession.interfaces.insert(*str);
1739b243a4eSEd Tanous }
1749b243a4eSEd Tanous }
1759b243a4eSEd Tanous }
1769b243a4eSEd Tanous
1779b243a4eSEd Tanous nlohmann::json::iterator paths = j.find("paths");
178418b934aSP Dheeraj Srujan Kumar if (paths == j.end())
1791abe55efSEd Tanous {
18062598e31SEd Tanous BMCWEB_LOG_ERROR("Unable to find paths in json data");
181418b934aSP Dheeraj Srujan Kumar conn.close("Unable to find paths in json data");
182418b934aSP Dheeraj Srujan Kumar return;
183418b934aSP Dheeraj Srujan Kumar }
184418b934aSP Dheeraj Srujan Kumar
185271584abSEd Tanous size_t interfaceCount = thisSession.interfaces.size();
1861abe55efSEd Tanous if (interfaceCount == 0)
1871abe55efSEd Tanous {
1889b243a4eSEd Tanous interfaceCount = 1;
1899b243a4eSEd Tanous }
190f8fe53e7SEd Tanous
1919b243a4eSEd Tanous // These regexes derived on the rules here:
1929b243a4eSEd Tanous // https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names
1934b242749SEd Tanous static std::regex validPath("^/([A-Za-z0-9_]+/?)*$");
1944b242749SEd Tanous static std::regex validInterface(
1959b243a4eSEd Tanous "^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)+$");
1969b243a4eSEd Tanous
1971abe55efSEd Tanous for (const auto& thisPath : *paths)
1981abe55efSEd Tanous {
1999b243a4eSEd Tanous const std::string* thisPathString =
2009b243a4eSEd Tanous thisPath.get_ptr<const std::string*>();
2011abe55efSEd Tanous if (thisPathString == nullptr)
2021abe55efSEd Tanous {
20362598e31SEd Tanous BMCWEB_LOG_ERROR("subscribe path isn't a string?");
2049b243a4eSEd Tanous conn.close();
2059b243a4eSEd Tanous return;
2069b243a4eSEd Tanous }
2071abe55efSEd Tanous if (!std::regex_match(*thisPathString, validPath))
2081abe55efSEd Tanous {
20962598e31SEd Tanous BMCWEB_LOG_ERROR("Invalid path name {}", *thisPathString);
2109b243a4eSEd Tanous conn.close();
2119b243a4eSEd Tanous return;
2129b243a4eSEd Tanous }
213f8fe53e7SEd Tanous std::string propertiesMatchString =
2149b243a4eSEd Tanous ("type='signal',"
2159b243a4eSEd Tanous "interface='org.freedesktop.DBus.Properties',"
2169b243a4eSEd Tanous "path_namespace='" +
2179b243a4eSEd Tanous *thisPathString +
2189b243a4eSEd Tanous "',"
2199b243a4eSEd Tanous "member='PropertiesChanged'");
2209b243a4eSEd Tanous // If interfaces weren't specified, add a single match for all
2219b243a4eSEd Tanous // interfaces
22226f6976fSEd Tanous if (thisSession.interfaces.empty())
2231abe55efSEd Tanous {
224bd79bce8SPatrick Williams BMCWEB_LOG_DEBUG("Creating match {}",
225bd79bce8SPatrick Williams propertiesMatchString);
2269b243a4eSEd Tanous
2279b243a4eSEd Tanous thisSession.matches.emplace_back(
22859d494eeSPatrick Williams std::make_unique<sdbusplus::bus::match_t>(
229bd79bce8SPatrick Williams *crow::connections::systemBus,
230bd79bce8SPatrick Williams propertiesMatchString, onPropertyUpdate, &conn));
2311abe55efSEd Tanous }
2321abe55efSEd Tanous else
2331abe55efSEd Tanous {
2341abe55efSEd Tanous // If interfaces were specified, add a match for each
2351abe55efSEd Tanous // interface
2361abe55efSEd Tanous for (const std::string& interface : thisSession.interfaces)
2371abe55efSEd Tanous {
2381abe55efSEd Tanous if (!std::regex_match(interface, validInterface))
2391abe55efSEd Tanous {
24062598e31SEd Tanous BMCWEB_LOG_ERROR("Invalid interface name {}",
24162598e31SEd Tanous interface);
2429b243a4eSEd Tanous conn.close();
2439b243a4eSEd Tanous return;
2449b243a4eSEd Tanous }
245f23b7296SEd Tanous std::string ifaceMatchString = propertiesMatchString;
246f23b7296SEd Tanous ifaceMatchString += ",arg0='";
247f23b7296SEd Tanous ifaceMatchString += interface;
248f23b7296SEd Tanous ifaceMatchString += "'";
24962598e31SEd Tanous BMCWEB_LOG_DEBUG("Creating match {}", ifaceMatchString);
2509b243a4eSEd Tanous thisSession.matches.emplace_back(
25159d494eeSPatrick Williams std::make_unique<sdbusplus::bus::match_t>(
2529b243a4eSEd Tanous *crow::connections::systemBus, ifaceMatchString,
2539b243a4eSEd Tanous onPropertyUpdate, &conn));
2549b243a4eSEd Tanous }
2559b243a4eSEd Tanous }
256f8fe53e7SEd Tanous std::string objectManagerMatchString =
2579b243a4eSEd Tanous ("type='signal',"
2589b243a4eSEd Tanous "interface='org.freedesktop.DBus.ObjectManager',"
2599b243a4eSEd Tanous "path_namespace='" +
2609b243a4eSEd Tanous *thisPathString +
2619b243a4eSEd Tanous "',"
2629b243a4eSEd Tanous "member='InterfacesAdded'");
26362598e31SEd Tanous BMCWEB_LOG_DEBUG("Creating match {}", objectManagerMatchString);
2649b243a4eSEd Tanous thisSession.matches.emplace_back(
26559d494eeSPatrick Williams std::make_unique<sdbusplus::bus::match_t>(
2662c70f800SEd Tanous *crow::connections::systemBus, objectManagerMatchString,
2672c70f800SEd Tanous onPropertyUpdate, &conn));
2689b243a4eSEd Tanous }
269911ac317SEd Tanous });
270911ac317SEd Tanous }
271aa2e59c1SEd Tanous } // namespace dbus_monitor
272911ac317SEd Tanous } // namespace crow
273