Add OemRouter facility.

OemRouter adds a facility to register OEM Group Message handlers,
then dispatch matching messages to the registered handler.
Added as a core source so that any dynamic provider can register its
messages without requiring any specific load order.

Includes code fixes for x86 portability.

Change-Id: I47b8fe7873e3c7fdf35a00d3c8a7e17d30c398c4
Signed-off-by: Peter Hanson <peterh@google.com>
Signed-off-by: Patrick Venture <venture@google.com>
diff --git a/Makefile.am b/Makefile.am
index ef1bf14..24fd5e7 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -8,7 +8,8 @@
 	settings.cpp \
 	host-cmd-manager.cpp \
 	timer.cpp \
-	utils.cpp
+	utils.cpp \
+	oemrouter.cpp
 nodist_ipmid_SOURCES = ipmiwhitelist.cpp
 
 libapphandler_BUILT_LIST = \
@@ -108,7 +109,9 @@
 nobase_include_HEADERS = \
 	host-ipmid/ipmid-api.h \
 	host-ipmid/ipmid-host-cmd.hpp \
-	host-ipmid/ipmid-host-cmd-utils.hpp
+	host-ipmid/ipmid-host-cmd-utils.hpp \
+	host-ipmid/oemopenbmc.hpp \
+	host-ipmid/oemrouter.hpp
 
 # Forcing the build of self and then subdir
 SUBDIRS = . test softoff
diff --git a/configure.ac b/configure.ac
index 9007582..918aef7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -29,7 +29,6 @@
 PKG_CHECK_MODULES([SDBUSPLUS], [sdbusplus],,\
     AC_MSG_ERROR(["Requires sdbusplus package."]))
 
-
 AS_IF([test "x$enable_softoff" != "xno"],
     # Check for sdbusplus
     PKG_CHECK_MODULES([SDBUSPLUS], [sdbusplus],, [AC_MSG_ERROR(["sdbusplus packaged required and not found"])])
@@ -50,8 +49,8 @@
 LT_LIB_DLLOAD
 
 # Check/set gtest specific functions.
-AX_PTHREAD([GTEST_CPPFLAGS="-DGTEST_HAS_PTHREAD=1"],[GTEST_CPPFLAGS="-DGTEST_HAS_PTHREAD=0"])
-AC_SUBST(GTEST_CPPFLAGS)
+PKG_CHECK_MODULES([GTEST], [gtest], [], [AC_MSG_NOTICE([gtest not found, tests will not build])])
+PKG_CHECK_MODULES([GTEST_MAIN], [gtest_main], [], [AC_MSG_NOTICE([gtest_main not found, tests will not build])])
 
 AC_ARG_ENABLE([oe-sdk],
     AS_HELP_STRING([--enable-oe-sdk], [Link testcases absolutely against OE SDK so they can be ran within it.])
diff --git a/host-ipmid/ipmid-api.h b/host-ipmid/ipmid-api.h
index 5a2e81d..1bec851 100644
--- a/host-ipmid/ipmid-api.h
+++ b/host-ipmid/ipmid-api.h
@@ -93,6 +93,7 @@
     NETFUN_STORAGE   =   0x0a,
     NETFUN_TRANSPORT =   0x0c,
     NETFUN_GRPEXT    =   0x2c,
+    NETFUN_OEM_GROUP =   0x2e,
     NETFUN_NONE      =   0x30,
     NETFUN_OEM       =   0x32,
     NETFUN_IBM_OEM   =   0x3A
diff --git a/host-ipmid/oemopenbmc.hpp b/host-ipmid/oemopenbmc.hpp
new file mode 100644
index 0000000..c3bfaa4
--- /dev/null
+++ b/host-ipmid/oemopenbmc.hpp
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "host-ipmid/ipmid-api.h"
+#include "host-ipmid/oemrouter.hpp"
+
+namespace oem
+{
+
+/*
+ * This is the OpenBMC IANA OEM Number
+ */
+constexpr Number obmcOemNumber = 49871;
+
+/*
+ * OpenBMC OEM Extension IPMI Command codes.
+ */
+enum Cmd
+{
+    gpioCmd = 1,
+    i2cCmd = 2,
+    flashCmd = 3,
+    fanManualCmd = 4,
+};
+
+}  // namespace oem
diff --git a/host-ipmid/oemrouter.hpp b/host-ipmid/oemrouter.hpp
new file mode 100644
index 0000000..bc2b88c
--- /dev/null
+++ b/host-ipmid/oemrouter.hpp
@@ -0,0 +1,83 @@
+#pragma once
+
+#include <array>
+#include <cstdint>
+#include <functional>
+#include <vector>
+
+#include "host-ipmid/ipmid-api.h"
+
+namespace oem
+{
+constexpr size_t groupMagicSize = 3;
+
+using Group = std::array<uint8_t, groupMagicSize>;
+using Number = uint32_t;  // smallest standard size >= 24.
+
+// Handler signature includes ipmi cmd to support wildcard cmd match.
+// Buffers and lengths exclude the OemGroup bytes in the IPMI message.
+// dataLen supplies length of reqBuf upon call, and should be set to the
+// length of replyBuf upon return - conventional in this code base.
+using Handler = std::function<ipmi_ret_t(
+                       ipmi_cmd_t,     // cmd byte
+                       const uint8_t*, // reqBuf
+                       uint8_t*,       // replyBuf
+                       size_t*)>;      // dataLen
+
+/// Router Interface class.
+/// @brief Abstract Router Interface
+class Router
+{
+    public:
+        virtual ~Router() {}
+
+        /// Enable message routing to begin.
+        virtual void activate() = 0;
+
+        /// Register a handler for given OEMNumber & cmd.
+        /// Use IPMI_CMD_WILDCARD to catch any unregistered cmd
+        /// for the given OEMNumber.
+        ///
+        /// @param[in] oen - the OEM Number.
+        /// @param[in] cmd - the Command.
+        /// @param[in] handler - the handler to call given that OEN and
+        ///                      command.
+        virtual void registerHandler(Number oen, ipmi_cmd_t cmd,
+                                     Handler handler) = 0;
+};
+
+/// Expose mutable Router for configuration & activation.
+///
+/// @returns pointer to OEM Router to use.
+Router* mutableRouter();
+
+/// Convert a group to an OEN.
+///
+/// @param[in] oeg - request buffer for IPMI command.
+/// @return the OEN.
+constexpr Number toOemNumber(const uint8_t oeg[groupMagicSize])
+{
+    return (oeg[2] << 16) | (oeg[1] << 8) | oeg[0];
+}
+
+/// Given a Group convert to an OEN.
+///
+/// @param[in] oeg - OEM Group reference.
+/// @return the OEN.
+constexpr Number toOemNumber(const Group& oeg)
+{
+    return (oeg[2] << 16) | (oeg[1] << 8) | oeg[0];
+}
+
+/// Given an OEN, conver to the OEM Group.
+///
+/// @param[in] oen - the OEM Number.
+/// @return the OEM Group.
+constexpr Group toOemGroup(Number oen)
+{
+    return Group { static_cast<uint8_t>(oen),
+                      static_cast<uint8_t>(oen >> 8),
+                      static_cast<uint8_t>(oen >> 16) };
+}
+
+}  // namespace oem
diff --git a/ipmid.cpp b/ipmid.cpp
index 3d7c663..f664488 100644
--- a/ipmid.cpp
+++ b/ipmid.cpp
@@ -27,6 +27,7 @@
 #include <host-cmd-manager.hpp>
 #include <host-ipmid/ipmid-host-cmd.hpp>
 #include <timer.hpp>
+#include "host-ipmid/oemrouter.hpp"
 
 using namespace phosphor::logging;
 namespace sdbusRule = sdbusplus::bus::match::rules;
@@ -616,6 +617,9 @@
     cmdManager = std::make_unique<phosphor::host::command::Manager>(
                             *sdbusp, events);
 
+    // Activate OemRouter.
+    oem::mutableRouter()->activate();
+
     // Register all the handlers that provider implementation to IPMI commands.
     ipmi_register_callback_handlers(HOST_IPMI_LIB_PATH);
 
diff --git a/oemrouter.cpp b/oemrouter.cpp
new file mode 100644
index 0000000..273a679
--- /dev/null
+++ b/oemrouter.cpp
@@ -0,0 +1,149 @@
+#include <cstdio>
+#include <cstring>
+#include <map>
+#include <utility>
+
+#include "host-ipmid/oemrouter.hpp"
+
+namespace oem
+{
+
+using Key = std::pair<Number, ipmi_cmd_t>;
+
+// Private implementation of OemRouter Interface.
+class RouterImpl : public Router
+{
+    public:
+        RouterImpl() {}
+
+        // Implement OemRouter Interface.
+        void activate() override;
+        void registerHandler(Number oen, ipmi_cmd_t cmd,
+                             Handler handler) override;
+
+        // Actual message routing function.
+        ipmi_ret_t routeMsg(ipmi_cmd_t cmd, const uint8_t* reqBuf,
+                            uint8_t* replyBuf, size_t* dataLen);
+
+    private:
+        std::map<Key, Handler> handlers;
+};
+
+// Static global instance for simplicity.
+static RouterImpl* globalRouterImpl;
+
+// TODO Refactor ipmid to avoid need for singleton here.
+Router* mutableRouter()
+{
+    if (!globalRouterImpl)
+    {
+        globalRouterImpl = new RouterImpl;
+    }
+    return globalRouterImpl;
+}
+
+ipmi_ret_t RouterImpl::routeMsg(ipmi_cmd_t cmd, const uint8_t* reqBuf,
+                                uint8_t* replyBuf, size_t* dataLen)
+{
+    // Not entirely clear we can route reply without complete OEM group.
+    // TODO: consider adding a way to suppress malformed replies.
+    if (*dataLen < groupMagicSize)
+    {
+        fprintf(stderr, "NetFn:[0x2E], OEM:[%zu bytes?], Cmd:[%#04X]\n",
+                *dataLen, cmd);
+        (*dataLen) = 0;
+        return IPMI_CC_REQ_DATA_LEN_INVALID;
+    }
+
+    // Find registered handler or reject request.
+    auto number = toOemNumber(reqBuf);
+    auto cmdKey = std::make_pair(number, cmd);
+
+    auto iter = handlers.find(cmdKey);
+    if (iter == handlers.end())
+    {
+        auto wildKey = std::make_pair(number, IPMI_CMD_WILDCARD);
+        iter = handlers.find(wildKey);
+        if (iter == handlers.end())
+        {
+            fprintf(stderr, "No Registered handler for NetFn:[0x2E], "
+                    "OEM:[%#08X], Cmd:[%#04X]\n", number, cmd);
+            *dataLen = groupMagicSize;
+            return IPMI_CC_INVALID;
+        }
+#ifdef __IPMI_DEBUG__
+        fprintf(stderr, "Wildcard NetFn:[0x2E], OEM:[%#08X], Cmd:[%#04X]\n",
+                number, cmd);
+#endif
+    }
+    else
+    {
+#ifdef __IPMI_DEBUG__
+        fprintf(stderr, "Match NetFn:[0x2E], OEM:[%#08X], Cmd:[%#04X]\n",
+                number, cmd);
+#endif
+    }
+
+    // Copy OEMGroup here, by analogy to IPMI CC code at netfn router;
+    // OemHandler should deal only with optional following data bytes.
+    std::memcpy(replyBuf, reqBuf, groupMagicSize);
+
+    size_t oemDataLen = *dataLen - groupMagicSize;
+    Handler& handler = iter->second;
+
+    auto rc = handler(cmd, reqBuf + groupMagicSize,
+                      replyBuf + groupMagicSize, &oemDataLen);
+
+    // Add OEMGroup bytes to nominal reply.
+    *dataLen = oemDataLen + groupMagicSize;
+    return rc;
+}
+
+// Function suitable for use as ipmi_netfn_router() call-back.
+// Translates call-back pointer args to more specific types.
+ipmi_ret_t ipmi_oem_wildcard_handler(ipmi_netfn_t /* netfn */,
+                                     ipmi_cmd_t cmd, ipmi_request_t request,
+                                     ipmi_response_t response,
+                                     ipmi_data_len_t dataLen,
+                                     ipmi_context_t context)
+{
+    // View requests & responses as byte sequences.
+    const uint8_t* reqBuf = static_cast<uint8_t*>(request);
+    uint8_t* replyBuf = static_cast<uint8_t*>(response);
+
+    // View context as router object, defaulting nullptr to global object.
+    auto router = static_cast<RouterImpl*>(
+            context ? context : mutableRouter());
+
+    // Send message parameters to dispatcher.
+    return router->routeMsg(cmd, reqBuf, replyBuf, dataLen);
+}
+
+// Enable message routing to begin.
+void RouterImpl::activate()
+{
+    // Register netfn 0x2e OEM Group, any (wildcard) command.
+    printf("Registering NetFn:[0x%X], Cmd:[0x%X]\n",
+           NETFUN_OEM_GROUP, IPMI_CMD_WILDCARD);
+    ipmi_register_callback(NETFUN_OEM_GROUP, IPMI_CMD_WILDCARD, this,
+                           ipmi_oem_wildcard_handler, PRIVILEGE_OEM);
+}
+
+void RouterImpl::registerHandler(Number oen, ipmi_cmd_t cmd,
+                                 Handler handler)
+{
+    auto cmdKey = std::make_pair(oen, cmd);
+    auto iter = handlers.find(cmdKey);
+    if (iter == handlers.end())
+    {
+        // Add handler if key not already taken.
+        handlers.emplace(cmdKey, handler);
+    }
+    else
+    {
+        fprintf(stderr, "ERROR : Duplicate registration for NetFn:[0x2E], "
+                "OEM:[%#08X], Cmd:[%#04X]\n", oen, cmd);
+    }
+}
+
+}  // namespace oem
diff --git a/test/Makefile.am b/test/Makefile.am
index cb5640e..cd42b26 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -1,8 +1,10 @@
-AM_CPPFLAGS = -I$(top_srcdir) $(CODE_COVERAGE_CPPFLAGS)
+AM_CPPFLAGS = -I$(top_srcdir) $(CODE_COVERAGE_CPPFLAGS) $(GTEST_CFLAGS)
 AM_CFLAGS = $(CODE_COVERAGE_CFLAGS)
-check_PROGRAMS =
+AM_CXXFLAGS = $(GTEST_MAIN_CFLAGS) $(GTEST_CFLAGS)
+AM_LDFLAGS = $(GTEST_MAIN_LIBS) $(OESDK_TESTCASE_FLAGS)
 
 # Run all 'check' test programs
+check_PROGRAMS =
 TESTS = $(check_PROGRAMS)
 
 # Build/add sample_unittest to test suite
@@ -14,3 +16,8 @@
 sample_unittest_SOURCES = %reldir%/sample_unittest.cpp
 sample_unittest_LDADD = $(top_builddir)/sample.o
 check_PROGRAMS += %reldir%/sample_unittest
+
+# Build/add oemrouter_unittest to test suite
+check_PROGRAMS += oemrouter_unittest
+oemrouter_unittest_SOURCES = oemrouter_unittest.cpp
+oemrouter_unittest_LDADD = $(top_builddir)/oemrouter.o
diff --git a/test/oemrouter_unittest.cpp b/test/oemrouter_unittest.cpp
new file mode 100644
index 0000000..a5a79dd
--- /dev/null
+++ b/test/oemrouter_unittest.cpp
@@ -0,0 +1,175 @@
+#include "host-ipmid/ipmid-api.h"
+#include "host-ipmid/oemrouter.hpp"
+#include "sample.h"
+
+#include <cstring>
+#include <gtest/gtest.h>
+
+// Watch for correct singleton behavior.
+static oem::Router* singletonUnderTest;
+
+static ipmid_callback_t wildHandler;
+
+static ipmi_netfn_t lastNetFunction;
+
+// Fake ipmi_register_callback() for this test.
+void ipmi_register_callback(ipmi_netfn_t netfn, ipmi_cmd_t cmd,
+                            ipmi_context_t context, ipmid_callback_t cb,
+                            ipmi_cmd_privilege_t priv)
+{
+    EXPECT_EQ(NETFUN_OEM_GROUP, netfn);
+    EXPECT_EQ(IPMI_CMD_WILDCARD, cmd);
+    EXPECT_EQ(reinterpret_cast<void*>(singletonUnderTest), context);
+    EXPECT_EQ(PRIVILEGE_OEM, priv);
+    lastNetFunction = netfn;
+    wildHandler = cb;
+}
+
+namespace oem
+{
+
+namespace
+{
+void MakeRouter()
+{
+    if (!singletonUnderTest)
+    {
+        singletonUnderTest = mutableRouter();
+    }
+    ASSERT_EQ(singletonUnderTest, mutableRouter());
+}
+
+void ActivateRouter()
+{
+    MakeRouter();
+    singletonUnderTest->activate();
+    ASSERT_EQ(NETFUN_OEM_GROUP, lastNetFunction);
+}
+
+void RegisterWithRouter(Number oen, ipmi_cmd_t cmd, Handler cb)
+{
+    ActivateRouter();
+    singletonUnderTest->registerHandler(oen, cmd, cb);
+}
+
+uint8_t msgPlain[] = { 0x56, 0x34, 0x12 };
+uint8_t replyPlain[] = { 0x56, 0x34, 0x12, 0x31, 0x41 };
+uint8_t msgPlus2[] = { 0x67, 0x45, 0x23, 0x10, 0x20 };
+uint8_t msgBadOen[] = { 0x57, 0x34, 0x12 };
+
+void RegisterTwoWays(ipmi_cmd_t *nextCmd)
+{
+    Handler f = [](ipmi_cmd_t cmd, const uint8_t* reqBuf,
+                   uint8_t* replyBuf, size_t* dataLen)
+    {
+        // Check inputs
+        EXPECT_EQ(0x78, cmd);
+        EXPECT_EQ(0, *dataLen);  // Excludes OEN
+
+        // Generate reply.
+        *dataLen = 2;
+        std::memcpy(replyBuf, replyPlain + 3, *dataLen);
+        return 0;
+    };
+    RegisterWithRouter(0x123456, 0x78, f);
+
+    *nextCmd = IPMI_CMD_WILDCARD;
+    Handler g = [nextCmd](ipmi_cmd_t cmd, const uint8_t* reqBuf,
+                          uint8_t* replyBuf, size_t* dataLen)
+    {
+        // Check inputs
+        EXPECT_EQ(*nextCmd, cmd);
+        EXPECT_EQ(2, *dataLen);  // Excludes OEN
+        if (2 != *dataLen)
+        {
+            return 0xE0;
+        }
+        EXPECT_EQ(msgPlus2[3], reqBuf[0]);
+        EXPECT_EQ(msgPlus2[4], reqBuf[1]);
+
+        // Generate reply.
+        *dataLen = 0;
+        return 0;
+    };
+    RegisterWithRouter(0x234567, IPMI_CMD_WILDCARD, g);
+}
+}  // namespace
+
+TEST(OemRouterTest, MakeRouterProducesConsistentSingleton) {
+    MakeRouter();
+}
+
+TEST(OemRouterTest, ActivateRouterSetsLastNetToOEMGROUP) {
+    lastNetFunction = 0;
+    ActivateRouter();
+}
+
+TEST(OemRouterTest, VerifiesSpecificCommandMatches) {
+    ipmi_cmd_t cmd;
+    uint8_t reply[256];
+    size_t dataLen;
+
+    RegisterTwoWays(&cmd);
+
+    dataLen = 3;
+    EXPECT_EQ(0,
+              wildHandler(NETFUN_OEM_GROUP, 0x78, msgPlain, reply,
+                          &dataLen, nullptr));
+    EXPECT_EQ(5, dataLen);
+    EXPECT_EQ(replyPlain[0], reply[0]);
+    EXPECT_EQ(replyPlain[1], reply[1]);
+    EXPECT_EQ(replyPlain[2], reply[2]);
+    EXPECT_EQ(replyPlain[3], reply[3]);
+    EXPECT_EQ(replyPlain[4], reply[4]);
+}
+
+TEST(OemRouterTest, WildCardMatchesTwoRandomCodes) {
+    ipmi_cmd_t cmd;
+    uint8_t reply[256];
+    size_t dataLen;
+
+    RegisterTwoWays(&cmd);
+
+    // Check two random command codes.
+    dataLen = 5;
+    cmd = 0x89;
+    EXPECT_EQ(0,
+              wildHandler(NETFUN_OEM_GROUP, cmd, msgPlus2, reply,
+                          &dataLen, nullptr));
+    EXPECT_EQ(3, dataLen);
+
+    dataLen = 5;
+    cmd = 0x67;
+    EXPECT_EQ(0,
+              wildHandler(NETFUN_OEM_GROUP, cmd, msgPlus2, reply,
+                          &dataLen, nullptr));
+    EXPECT_EQ(3, dataLen);
+}
+
+TEST(OemRouterTest, CommandsAreRejectedIfInvalid) {
+    ipmi_cmd_t cmd;
+    uint8_t reply[256];
+    size_t dataLen;
+
+    RegisterTwoWays(&cmd);
+
+    // Message too short to include whole OEN?
+    dataLen = 2;
+    EXPECT_EQ(IPMI_CC_REQ_DATA_LEN_INVALID,
+              wildHandler(NETFUN_OEM_GROUP, 0x78, msgPlain, reply,
+                          &dataLen, nullptr));
+
+    // Wrong specific command?
+    dataLen = 3;
+    EXPECT_EQ(IPMI_CC_INVALID,
+              wildHandler(NETFUN_OEM_GROUP, 0x89, msgPlain, reply,
+                          &dataLen, nullptr));
+
+    // Wrong OEN?
+    dataLen = 3;
+    EXPECT_EQ(IPMI_CC_INVALID,
+              wildHandler(NETFUN_OEM_GROUP, 0x78, msgBadOen, reply,
+                          &dataLen, nullptr));
+}
+
+}  // namespace oem