look up channel from D-Bus connection

With a single IPMI execution queue and multiple bridges, it is important
for the queue to be able to map the incoming D-Bus message to a channel.
Each bridge should request a well-known name of the form
"xyz.openbmc_project.Ipmi.Channel.<name>" where name is the name field
in the /usr/share/ipmi-providers/channel_config.json file.

The mapping is done by registering a name change listener on D-Bus and
then doing some name lookups via the D-Bus interface to match well-known
names to unique names. Then, each incoming messages comes from a
unique-named sender and can be resolved to which channel it came from.

For now, any unmapped channels will show up as INTRABMC, which is not
ideal, but should not break things until the bridges can be converted to
the correct name and D-Bus API.

Tested-by: run ipmid, start and stop bridges named
           xyz.openbmc_project.Ipmi.Channel.<NAME> and see the messages
           show that a new channel has been mapped. Then, with commands
           that use the ipmi::Context, the correct channel shows up.

Change-Id: I3e6bbfbf2e068020e07eeafe64eb09d70c03dc65
Signed-off-by: Vernon Mauery <vernon.mauery@linux.intel.com>
diff --git a/ipmid-new.cpp b/ipmid-new.cpp
index 2a2d4a6..403d4d1 100644
--- a/ipmid-new.cpp
+++ b/ipmid-new.cpp
@@ -21,6 +21,7 @@
 
 #include <algorithm>
 #include <any>
+#include <boost/algorithm/string.hpp>
 #include <dcmihandler.hpp>
 #include <exception>
 #include <filesystem>
@@ -320,21 +321,215 @@
     return executeIpmiCommandCommon(handlerMap, netFn, request);
 }
 
+namespace utils
+{
+template <typename AssocContainer, typename UnaryPredicate>
+void assoc_erase_if(AssocContainer& c, UnaryPredicate p)
+{
+    typename AssocContainer::iterator next = c.begin();
+    typename AssocContainer::iterator last = c.end();
+    while ((next = std::find_if(next, last, p)) != last)
+    {
+        c.erase(next++);
+    }
+}
+} // namespace utils
+
+namespace
+{
+std::unordered_map<std::string, uint8_t> uniqueNameToChannelNumber;
+
+// sdbusplus::bus::match::rules::arg0namespace() wants the prefix
+// to match without any trailing '.'
+constexpr const char ipmiDbusChannelMatch[] =
+    "xyz.openbmc_project.Ipmi.Channel";
+void updateOwners(sdbusplus::asio::connection& conn, const std::string& name)
+{
+    conn.async_method_call(
+        [name](const boost::system::error_code ec,
+               const std::string& nameOwner) {
+            if (ec)
+            {
+                log<level::ERR>("Error getting dbus owner",
+                                entry("INTERFACE=%s", name.c_str()));
+                return;
+            }
+            // start after ipmiDbusChannelPrefix (after the '.')
+            std::string chName =
+                name.substr(std::strlen(ipmiDbusChannelMatch) + 1);
+            try
+            {
+                uint8_t channel = getChannelByName(chName);
+                uniqueNameToChannelNumber[nameOwner] = channel;
+                log<level::INFO>("New interface mapping",
+                                 entry("INTERFACE=%s", name.c_str()),
+                                 entry("CHANNEL=%u", channel));
+            }
+            catch (const std::exception& e)
+            {
+                log<level::INFO>("Failed interface mapping, no such name",
+                                 entry("INTERFACE=%s", name.c_str()));
+            }
+        },
+        "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetNameOwner",
+        name);
+}
+
+void doListNames(boost::asio::io_service& io, sdbusplus::asio::connection& conn)
+{
+    conn.async_method_call(
+        [&io, &conn](const boost::system::error_code ec,
+                     std::vector<std::string> busNames) {
+            if (ec)
+            {
+                log<level::ERR>("Error getting dbus names");
+                std::exit(EXIT_FAILURE);
+                return;
+            }
+            // Try to make startup consistent
+            std::sort(busNames.begin(), busNames.end());
+
+            const std::string channelPrefix =
+                std::string(ipmiDbusChannelMatch) + ".";
+            for (const std::string& busName : busNames)
+            {
+                if (busName.find(channelPrefix) == 0)
+                {
+                    updateOwners(conn, busName);
+                }
+            }
+        },
+        "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus",
+        "ListNames");
+}
+
+void nameChangeHandler(sdbusplus::message::message& message)
+{
+    std::string name;
+    std::string oldOwner;
+    std::string newOwner;
+
+    message.read(name, oldOwner, newOwner);
+
+    if (!oldOwner.empty())
+    {
+        if (boost::starts_with(oldOwner, ":"))
+        {
+            // Connection removed
+            auto it = uniqueNameToChannelNumber.find(oldOwner);
+            if (it != uniqueNameToChannelNumber.end())
+            {
+                uniqueNameToChannelNumber.erase(it);
+            }
+        }
+    }
+    if (!newOwner.empty())
+    {
+        // start after ipmiDbusChannelMatch (and after the '.')
+        std::string chName = name.substr(std::strlen(ipmiDbusChannelMatch) + 1);
+        try
+        {
+            uint8_t channel = getChannelByName(chName);
+            uniqueNameToChannelNumber[newOwner] = channel;
+            log<level::INFO>("New interface mapping",
+                             entry("INTERFACE=%s", name.c_str()),
+                             entry("CHANNEL=%u", channel));
+        }
+        catch (const std::exception& e)
+        {
+            log<level::INFO>("Failed interface mapping, no such name",
+                             entry("INTERFACE=%s", name.c_str()));
+        }
+    }
+};
+
+} // anonymous namespace
+
+static constexpr const char intraBmcName[] = "INTRABMC";
+uint8_t channelFromMessage(sdbusplus::message::message& msg)
+{
+    // channel name for ipmitool to resolve to
+    std::string sender = msg.get_sender();
+    auto chIter = uniqueNameToChannelNumber.find(sender);
+    if (chIter != uniqueNameToChannelNumber.end())
+    {
+        return chIter->second;
+    }
+    // FIXME: currently internal connections are ephemeral and hard to pin down
+    try
+    {
+        return getChannelByName(intraBmcName);
+    }
+    catch (const std::exception& e)
+    {
+        return invalidChannel;
+    }
+} // namespace ipmi
+
 /* called from sdbus async server context */
-auto executionEntry(boost::asio::yield_context yield, NetFn netFn, uint8_t lun,
+auto executionEntry(boost::asio::yield_context yield,
+                    sdbusplus::message::message& m, NetFn netFn, uint8_t lun,
                     Cmd cmd, std::vector<uint8_t>& data,
                     std::map<std::string, ipmi::Value>& options)
 {
-    auto ctx = std::make_shared<ipmi::Context>(netFn, cmd, 0, 0,
-                                               ipmi::Privilege::Admin, &yield);
+    const auto dbusResponse =
+        [netFn, lun, cmd](Cc cc, const std::vector<uint8_t>& data = {}) {
+            constexpr uint8_t netFnResponse = 0x01;
+            uint8_t retNetFn = netFn | netFnResponse;
+            return std::make_tuple(retNetFn, lun, cmd, cc, data);
+        };
+    std::string sender = m.get_sender();
+    Privilege privilege = Privilege::None;
+    uint8_t userId = 0; // undefined user
+
+    // figure out what channel the request came in on
+    uint8_t channel = channelFromMessage(m);
+    if (channel == invalidChannel)
+    {
+        // unknown sender channel; refuse to service the request
+        log<level::ERR>("ERROR determining source IPMI channel",
+                        entry("SENDER=%s", sender.c_str()),
+                        entry("NETFN=0x%X", netFn), entry("CMD=0x%X", cmd));
+        return dbusResponse(ipmi::ccDestinationUnavailable);
+    }
+
+    // session-based channels are required to provide userId/privilege
+    if (getChannelSessionSupport(channel) != EChannelSessSupported::none)
+    {
+        try
+        {
+            Value requestPriv = options.at("privilege");
+            Value requestUserId = options.at("userId");
+            privilege = static_cast<Privilege>(std::get<int>(requestPriv));
+            userId = static_cast<uint8_t>(std::get<int>(requestUserId));
+        }
+        catch (const std::exception& e)
+        {
+            log<level::ERR>("ERROR determining IPMI session credentials",
+                            entry("CHANNEL=%u", channel),
+                            entry("NETFN=0x%X", netFn), entry("CMD=0x%X", cmd));
+            return dbusResponse(ipmi::ccUnspecifiedError);
+        }
+    }
+    else
+    {
+        // get max privilege for session-less channels
+        // For now, there is not a way to configure this, default to Admin
+        privilege = Privilege::Admin;
+    }
+    // check to see if the requested priv/username is valid
+    log<level::DEBUG>("Set up ipmi context", entry("SENDER=%s", sender.c_str()),
+                      entry("NETFN=0x%X", netFn), entry("CMD=0x%X", cmd),
+                      entry("CHANNEL=%u", channel), entry("USERID=%u", userId),
+                      entry("PRIVILEGE=%u", static_cast<uint8_t>(privilege)));
+
+    auto ctx = std::make_shared<ipmi::Context>(netFn, cmd, channel, userId,
+                                               privilege, &yield);
     auto request = std::make_shared<ipmi::message::Request>(
         ctx, std::forward<std::vector<uint8_t>>(data));
     message::Response::ptr response = executeIpmiCommand(request);
 
-    // Responses in IPMI require a bit set.  So there ya go...
-    netFn |= 0x01;
-    return std::make_tuple(netFn, lun, cmd, response->cc,
-                           response->payload.raw);
+    return dbusResponse(response->cc, response->payload.raw);
 }
 
 /** @struct IpmiProvider
@@ -623,6 +818,15 @@
                                                   handleLegacyIpmiCommand);
 #endif /* ALLOW_DEPRECATED_API */
 
+    // set up bus name watching to match channels with bus names
+    sdbusplus::bus::match::match nameOwnerChanged(
+        *sdbusp,
+        sdbusplus::bus::match::rules::nameOwnerChanged() +
+            sdbusplus::bus::match::rules::arg0namespace(
+                ipmi::ipmiDbusChannelMatch),
+        ipmi::nameChangeHandler);
+    ipmi::doListNames(*io, *sdbusp);
+
     // set up boost::asio signal handling
     std::function<SignalResponse(int)> stopAsioRunLoop =
         [&io](int signalNumber) {