EventService: Manager and subscriptions support

Add EventService Manager which will manage all the
EventService configuration and subscriptions. This
includes API for add or update or delete subscriptions
along with other supported API support. Also includes
http connection open and send event code using
"push style eventing".
Added BMCWEB_INSECURE_HTTP_PUSH_STYLE_EVENTING
build flag to enable/disable http push style eventing
which is not a secure channel.

Tested:
 - Tested along with other patches such as http
   client and Event log support, SubmitTestEvent
   and its works.
 - Ran Redfish validation successfully.

Change-Id: Ie4687e4cbfabd525b7a8ad4e615510f034edc6e9
Signed-off-by: AppaRao Puli <apparao.puli@linux.intel.com>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9d4e4e9..be9a374 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -127,6 +127,11 @@
 )
 
 option (
+    BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING
+    "Enable HTTP push style eventing feature" OFF
+)
+
+option (
     BMCWEB_ENABLE_VALIDATION_UNSECURE_FEATURE
     "Enables unsecure features required by validation. Note: must
     be turned off for production images."
@@ -416,6 +421,8 @@
     -DBMCWEB_INSECURE_UNRESTRICTED_SENSOR_OVERRIDE>
     $<$<BOOL:${BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE}>:
     -DBMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE>
+    $<$<BOOL:${BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING}>:
+    -DBMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING>
 )
 
 # configure and install systemd unit files
diff --git a/redfish-core/include/event_service_manager.hpp b/redfish-core/include/event_service_manager.hpp
new file mode 100644
index 0000000..9b907ba
--- /dev/null
+++ b/redfish-core/include/event_service_manager.hpp
@@ -0,0 +1,213 @@
+/*
+// Copyright (c) 2020 Intel Corporation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+*/
+#pragma once
+#include "node.hpp"
+
+#include <boost/container/flat_map.hpp>
+#include <cstdlib>
+#include <ctime>
+#include <error_messages.hpp>
+#include <http_client.hpp>
+#include <memory>
+#include <utils/json_utils.hpp>
+#include <variant>
+
+namespace redfish
+{
+class Subscription
+{
+  public:
+    std::string id;
+    std::string destinationUrl;
+    std::string protocol;
+    std::string retryPolicy;
+    std::string customText;
+    std::string eventFormatType;
+    std::string subscriptionType;
+    std::vector<std::string> registryMsgIds;
+    std::vector<std::string> registryPrefixes;
+    std::vector<nlohmann::json> httpHeaders; // key-value pair
+
+    Subscription(const Subscription&) = delete;
+    Subscription& operator=(const Subscription&) = delete;
+    Subscription(Subscription&&) = delete;
+    Subscription& operator=(Subscription&&) = delete;
+
+    Subscription(const std::string& inHost, const std::string& inPort,
+                 const std::string& inPath, const std::string& inUriProto) :
+        host(inHost),
+        port(inPort), path(inPath), uriProto(inUriProto)
+    {
+        conn = std::make_shared<crow::HttpClient>(
+            crow::connections::systemBus->get_io_context(), host, port);
+    }
+    ~Subscription()
+    {
+    }
+
+    void sendEvent(const std::string& msg)
+    {
+        std::vector<std::pair<std::string, std::string>> reqHeaders;
+        for (const auto& header : httpHeaders)
+        {
+            for (const auto& item : header.items())
+            {
+                std::string key = item.key();
+                std::string val = item.value();
+                reqHeaders.emplace_back(std::pair(key, val));
+            }
+        }
+        conn->setHeaders(reqHeaders);
+        conn->doConnectAndSend(path, msg);
+    }
+
+  private:
+    std::string host;
+    std::string port;
+    std::string path;
+    std::string uriProto;
+    std::shared_ptr<crow::HttpClient> conn;
+};
+
+class EventServiceManager
+{
+  private:
+    EventServiceManager(const EventServiceManager&) = delete;
+    EventServiceManager& operator=(const EventServiceManager&) = delete;
+    EventServiceManager(EventServiceManager&&) = delete;
+    EventServiceManager& operator=(EventServiceManager&&) = delete;
+
+    EventServiceManager()
+    {
+        // TODO: Read the persistent data from store and populate.
+        // Populating with default.
+        enabled = true;
+        retryAttempts = 3;
+        retryTimeoutInterval = 30; // seconds
+    }
+
+    boost::container::flat_map<std::string, std::shared_ptr<Subscription>>
+        subscriptionsMap;
+
+  public:
+    bool enabled;
+    uint32_t retryAttempts;
+    uint32_t retryTimeoutInterval;
+
+    static EventServiceManager& getInstance()
+    {
+        static EventServiceManager handler;
+        return handler;
+    }
+
+    void updateSubscriptionData()
+    {
+        // Persist the config and subscription data.
+        // TODO: subscriptionsMap & configData need to be
+        // written to Persist store.
+        return;
+    }
+
+    std::shared_ptr<Subscription> getSubscription(const std::string& id)
+    {
+        auto obj = subscriptionsMap.find(id);
+        if (obj == subscriptionsMap.end())
+        {
+            BMCWEB_LOG_ERROR << "No subscription exist with ID:" << id;
+            return nullptr;
+        }
+        std::shared_ptr<Subscription> subValue = obj->second;
+        return subValue;
+    }
+
+    std::string addSubscription(const std::shared_ptr<Subscription> subValue)
+    {
+        std::srand(static_cast<uint32_t>(std::time(0)));
+        std::string id;
+
+        int retry = 3;
+        while (retry)
+        {
+            id = std::to_string(std::rand());
+            auto inserted = subscriptionsMap.insert(std::pair(id, subValue));
+            if (inserted.second)
+            {
+                break;
+            }
+            --retry;
+        };
+
+        if (retry <= 0)
+        {
+            BMCWEB_LOG_ERROR << "Failed to generate random number";
+            return std::string("");
+        }
+
+        updateSubscriptionData();
+        return id;
+    }
+
+    bool isSubscriptionExist(const std::string& id)
+    {
+        auto obj = subscriptionsMap.find(id);
+        if (obj == subscriptionsMap.end())
+        {
+            return false;
+        }
+        return true;
+    }
+
+    void deleteSubscription(const std::string& id)
+    {
+        auto obj = subscriptionsMap.find(id);
+        if (obj != subscriptionsMap.end())
+        {
+            subscriptionsMap.erase(obj);
+            updateSubscriptionData();
+        }
+    }
+
+    size_t getNumberOfSubscriptions()
+    {
+        return subscriptionsMap.size();
+    }
+
+    std::vector<std::string> getAllIDs()
+    {
+        std::vector<std::string> idList;
+        for (const auto& it : subscriptionsMap)
+        {
+            idList.emplace_back(it.first);
+        }
+        return idList;
+    }
+
+    bool isDestinationExist(const std::string& destUrl)
+    {
+        for (const auto& it : subscriptionsMap)
+        {
+            std::shared_ptr<Subscription> entry = it.second;
+            if (entry->destinationUrl == destUrl)
+            {
+                BMCWEB_LOG_ERROR << "Destination exist already" << destUrl;
+                return true;
+            }
+        }
+        return false;
+    }
+};
+
+} // namespace redfish
diff --git a/redfish-core/lib/event_service.hpp b/redfish-core/lib/event_service.hpp
index 6265491..43517dd 100644
--- a/redfish-core/lib/event_service.hpp
+++ b/redfish-core/lib/event_service.hpp
@@ -14,12 +14,7 @@
 // limitations under the License.
 */
 #pragma once
-#include "node.hpp"
-
-#include <boost/container/flat_map.hpp>
-#include <error_messages.hpp>
-#include <utils/json_utils.hpp>
-#include <variant>
+#include "event_service_manager.hpp"
 
 namespace redfish
 {
@@ -33,52 +28,11 @@
 
 static constexpr const uint8_t maxNoOfSubscriptions = 20;
 
-struct EventSrvConfig
-{
-    bool enabled;
-    uint32_t retryAttempts;
-    uint32_t retryTimeoutInterval;
-};
-
-struct EventSrvSubscription
-{
-    std::string destinationUrl;
-    std::string protocol;
-    std::string retryPolicy;
-    std::string customText;
-    std::string eventFormatType;
-    std::string subscriptionType;
-    std::vector<std::string> registryMsgIds;
-    std::vector<std::string> registryPrefixes;
-    std::vector<nlohmann::json> httpHeaders; // key-value pair
-};
-
-EventSrvConfig configData;
-boost::container::flat_map<std::string, EventSrvSubscription> subscriptionsMap;
-
-inline void initEventSrvStore()
-{
-    // TODO: Read the persistent data from store and populate.
-    // Populating with default.
-    configData.enabled = true;
-    configData.retryAttempts = 3;
-    configData.retryTimeoutInterval = 30; // seconds
-}
-
-inline void updateSubscriptionData()
-{
-    // Persist the config and subscription data.
-    // TODO: subscriptionsMap & configData need to be
-    // written to Persist store.
-    return;
-}
 class EventService : public Node
 {
   public:
     EventService(CrowApp& app) : Node(app, "/redfish/v1/EventService/")
     {
-        initEventSrvStore();
-
         entityPrivileges = {
             {boost::beast::http::verb::get, {{"Login"}}},
             {boost::beast::http::verb::head, {{"Login"}}},
@@ -104,11 +58,12 @@
             {"@odata.id", "/redfish/v1/EventService"}};
 
         asyncResp->res.jsonValue["Status"]["State"] = "Enabled";
-        asyncResp->res.jsonValue["ServiceEnabled"] = configData.enabled;
+        asyncResp->res.jsonValue["ServiceEnabled"] =
+            EventServiceManager::getInstance().enabled;
         asyncResp->res.jsonValue["DeliveryRetryAttempts"] =
-            configData.retryAttempts;
+            EventServiceManager::getInstance().retryAttempts;
         asyncResp->res.jsonValue["DeliveryRetryIntervalSeconds"] =
-            configData.retryTimeoutInterval;
+            EventServiceManager::getInstance().retryTimeoutInterval;
         asyncResp->res.jsonValue["EventFormatTypes"] = supportedEvtFormatTypes;
         asyncResp->res.jsonValue["RegistryPrefixes"] = supportedRegPrefixes;
     }
@@ -131,7 +86,7 @@
 
         if (serviceEnabled)
         {
-            configData.enabled = *serviceEnabled;
+            EventServiceManager::getInstance().enabled = *serviceEnabled;
         }
 
         if (retryAttemps)
@@ -145,7 +100,8 @@
             }
             else
             {
-                configData.retryAttempts = *retryAttemps;
+                EventServiceManager::getInstance().retryAttempts =
+                    *retryAttemps;
             }
         }
 
@@ -160,11 +116,12 @@
             }
             else
             {
-                configData.retryTimeoutInterval = *retryInterval;
+                EventServiceManager::getInstance().retryTimeoutInterval =
+                    *retryInterval;
             }
         }
 
-        updateSubscriptionData();
+        EventServiceManager::getInstance().updateSubscriptionData();
     }
 };
 
@@ -196,15 +153,17 @@
             {"Name", "Event Destination Collections"}};
 
         nlohmann::json& memberArray = asyncResp->res.jsonValue["Members"];
-        memberArray = nlohmann::json::array();
-        asyncResp->res.jsonValue["Members@odata.count"] =
-            subscriptionsMap.size();
 
-        for (auto& it : subscriptionsMap)
+        std::vector<std::string> subscripIds =
+            EventServiceManager::getInstance().getAllIDs();
+        memberArray = nlohmann::json::array();
+        asyncResp->res.jsonValue["Members@odata.count"] = subscripIds.size();
+
+        for (const std::string& id : subscripIds)
         {
             memberArray.push_back(
                 {{"@odata.id",
-                  "/redfish/v1/EventService/Subscriptions/" + it.first}});
+                  "/redfish/v1/EventService/Subscriptions/" + id}});
         }
     }
 
@@ -213,7 +172,8 @@
     {
         auto asyncResp = std::make_shared<AsyncResp>(res);
 
-        if (subscriptionsMap.size() >= maxNoOfSubscriptions)
+        if (EventServiceManager::getInstance().getNumberOfSubscriptions() >=
+            maxNoOfSubscriptions)
         {
             messages::eventSubscriptionLimitExceeded(asyncResp->res);
             return;
@@ -238,19 +198,58 @@
             return;
         }
 
-        EventSrvSubscription subValue;
-
         // Validate the URL using regex expression
-        // Format: <protocol>://<host>:<port>/uri
-        // protocol: http/https, uri: can include params.
-        const std::regex urlRegex("(http|https)://([^/ :]+):?.*");
-        if (!std::regex_match(destUrl, urlRegex))
+        // Format: <protocol>://<host>:<port>/<uri>
+        // protocol: http/https
+        // host: Exclude ' ', ':', '#', '?'
+        // port: Empty or numeric value with ':' seperator.
+        // uri: Start with '/' and Exclude '#', ' '
+        //      Can include query params(ex: '/event?test=1')
+        // TODO: Need to validate hostname extensively(as per rfc)
+        const std::regex urlRegex(
+            "(http|https)://([^/\\x20\\x3f\\x23\\x3a]+):?([0-9]*)(/"
+            "([^\\x20\\x23\\x3f]*\\x3f?([^\\x20\\x23\\x3f])*)?)");
+        std::cmatch match;
+        if (!std::regex_match(destUrl.c_str(), match, urlRegex))
         {
             messages::propertyValueFormatError(asyncResp->res, destUrl,
                                                "Destination");
             return;
         }
-        subValue.destinationUrl = destUrl;
+
+        std::string uriProto = std::string(match[1].first, match[1].second);
+        if (uriProto == "http")
+        {
+#ifndef BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING
+            messages::propertyValueFormatError(asyncResp->res, destUrl,
+                                               "Destination");
+            return;
+#endif
+        }
+
+        std::string host = std::string(match[2].first, match[2].second);
+        std::string port = std::string(match[3].first, match[3].second);
+        std::string path = std::string(match[4].first, match[4].second);
+        if (port.empty())
+        {
+            if (uriProto == "http")
+            {
+                port = "80";
+            }
+            else
+            {
+                port = "443";
+            }
+        }
+        if (path.empty())
+        {
+            path = "/";
+        }
+
+        std::shared_ptr<Subscription> subValue =
+            std::make_shared<Subscription>(host, port, path, uriProto);
+
+        subValue->destinationUrl = destUrl;
 
         if (subscriptionType)
         {
@@ -260,11 +259,11 @@
                     asyncResp->res, *subscriptionType, "SubscriptionType");
                 return;
             }
-            subValue.subscriptionType = *subscriptionType;
+            subValue->subscriptionType = *subscriptionType;
         }
         else
         {
-            subValue.subscriptionType = "RedfishEvent"; // Default
+            subValue->subscriptionType = "RedfishEvent"; // Default
         }
 
         if (protocol != "Redfish")
@@ -273,7 +272,7 @@
                                              "Protocol");
             return;
         }
-        subValue.protocol = protocol;
+        subValue->protocol = protocol;
 
         if (eventFormatType)
         {
@@ -285,22 +284,22 @@
                     asyncResp->res, *eventFormatType, "EventFormatType");
                 return;
             }
-            subValue.eventFormatType = *eventFormatType;
+            subValue->eventFormatType = *eventFormatType;
         }
         else
         {
             // If not specified, use default "Event"
-            subValue.eventFormatType.assign({"Event"});
+            subValue->eventFormatType.assign({"Event"});
         }
 
         if (context)
         {
-            subValue.customText = *context;
+            subValue->customText = *context;
         }
 
         if (headers)
         {
-            subValue.httpHeaders = *headers;
+            subValue->httpHeaders = *headers;
         }
 
         if (regPrefixes)
@@ -316,14 +315,14 @@
                     return;
                 }
             }
-            subValue.registryPrefixes = *regPrefixes;
+            subValue->registryPrefixes = *regPrefixes;
         }
 
         if (msgIds)
         {
             // Do we need to loop-up MessageRegistry and validate
             // data for authenticity??? Not mandate, i believe.
-            subValue.registryMsgIds = *msgIds;
+            subValue->registryMsgIds = *msgIds;
         }
 
         if (retryPolicy)
@@ -336,37 +335,22 @@
                                                  "DeliveryRetryPolicy");
                 return;
             }
-            subValue.retryPolicy = *retryPolicy;
+            subValue->retryPolicy = *retryPolicy;
         }
         else
         {
             // Default "TerminateAfterRetries"
-            subValue.retryPolicy = "TerminateAfterRetries";
+            subValue->retryPolicy = "TerminateAfterRetries";
         }
 
-        std::srand(static_cast<uint32_t>(std::time(0)));
-        std::string id;
-
-        int retry = 3;
-        while (retry)
-        {
-            id = std::to_string(std::rand());
-            auto inserted = subscriptionsMap.insert(std::pair(id, subValue));
-            if (inserted.second)
-            {
-                break;
-            }
-            retry--;
-        };
-
-        if (retry <= 0)
+        std::string id =
+            EventServiceManager::getInstance().addSubscription(subValue);
+        if (id.empty())
         {
             messages::internalError(asyncResp->res);
             return;
         }
 
-        updateSubscriptionData();
-
         messages::created(asyncResp->res);
         asyncResp->res.addHeader(
             "Location", "/redfish/v1/EventService/Subscriptions/" + id);
@@ -400,16 +384,15 @@
             return;
         }
 
-        const std::string& id = params[0];
-        auto obj = subscriptionsMap.find(id);
-        if (obj == subscriptionsMap.end())
+        std::shared_ptr<Subscription> subValue =
+            EventServiceManager::getInstance().getSubscription(params[0]);
+        if (subValue == nullptr)
         {
             res.result(boost::beast::http::status::not_found);
             res.end();
             return;
         }
-
-        EventSrvSubscription& subValue = obj->second;
+        const std::string& id = params[0];
 
         res.jsonValue = {
             {"@odata.type", "#EventDestination.v1_7_0.EventDestination"},
@@ -418,16 +401,16 @@
             "/redfish/v1/EventService/Subscriptions/" + id;
         asyncResp->res.jsonValue["Id"] = id;
         asyncResp->res.jsonValue["Name"] = "Event Destination " + id;
-        asyncResp->res.jsonValue["Destination"] = subValue.destinationUrl;
-        asyncResp->res.jsonValue["Context"] = subValue.customText;
+        asyncResp->res.jsonValue["Destination"] = subValue->destinationUrl;
+        asyncResp->res.jsonValue["Context"] = subValue->customText;
         asyncResp->res.jsonValue["SubscriptionType"] =
-            subValue.subscriptionType;
-        asyncResp->res.jsonValue["HttpHeaders"] = subValue.httpHeaders;
-        asyncResp->res.jsonValue["EventFormatType"] = subValue.eventFormatType;
+            subValue->subscriptionType;
+        asyncResp->res.jsonValue["HttpHeaders"] = subValue->httpHeaders;
+        asyncResp->res.jsonValue["EventFormatType"] = subValue->eventFormatType;
         asyncResp->res.jsonValue["RegistryPrefixes"] =
-            subValue.registryPrefixes;
-        asyncResp->res.jsonValue["MessageIds"] = subValue.registryMsgIds;
-        asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue.retryPolicy;
+            subValue->registryPrefixes;
+        asyncResp->res.jsonValue["MessageIds"] = subValue->registryMsgIds;
+        asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue->retryPolicy;
     }
 
     void doPatch(crow::Response& res, const crow::Request& req,
@@ -440,9 +423,9 @@
             return;
         }
 
-        const std::string& id = params[0];
-        auto obj = subscriptionsMap.find(id);
-        if (obj == subscriptionsMap.end())
+        std::shared_ptr<Subscription> subValue =
+            EventServiceManager::getInstance().getSubscription(params[0]);
+        if (subValue == nullptr)
         {
             res.result(boost::beast::http::status::not_found);
             res.end();
@@ -460,16 +443,14 @@
             return;
         }
 
-        EventSrvSubscription& subValue = obj->second;
-
         if (context)
         {
-            subValue.customText = *context;
+            subValue->customText = *context;
         }
 
         if (headers)
         {
-            subValue.httpHeaders = *headers;
+            subValue->httpHeaders = *headers;
         }
 
         if (retryPolicy)
@@ -482,10 +463,10 @@
                                                  "DeliveryRetryPolicy");
                 return;
             }
-            subValue.retryPolicy = *retryPolicy;
+            subValue->retryPolicy = *retryPolicy;
         }
 
-        updateSubscriptionData();
+        EventServiceManager::getInstance().updateSubscriptionData();
     }
 
     void doDelete(crow::Response& res, const crow::Request& req,
@@ -499,18 +480,13 @@
             return;
         }
 
-        const std::string& id = params[0];
-        auto obj = subscriptionsMap.find(id);
-        if (obj == subscriptionsMap.end())
+        if (!EventServiceManager::getInstance().isSubscriptionExist(params[0]))
         {
             res.result(boost::beast::http::status::not_found);
             res.end();
             return;
         }
-
-        subscriptionsMap.erase(obj);
-
-        updateSubscriptionData();
+        EventServiceManager::getInstance().deleteSubscription(params[0]);
     }
 };