1 /*
2 // Copyright (c) 2020 Intel Corporation
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 //      http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 */
16 #pragma once
17 #include "event_service_manager.hpp"
18 
19 namespace redfish
20 {
21 
22 static constexpr const std::array<const char*, 2> supportedEvtFormatTypes = {
23     eventFormatType, metricReportFormatType};
24 static constexpr const std::array<const char*, 3> supportedRegPrefixes = {
25     "Base", "OpenBMC", "Task"};
26 static constexpr const std::array<const char*, 3> supportedRetryPolicies = {
27     "TerminateAfterRetries", "SuspendRetries", "RetryForever"};
28 
29 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE
30 static constexpr const std::array<const char*, 2> supportedResourceTypes = {
31     "IBMConfigFile", "Task"};
32 #else
33 static constexpr const std::array<const char*, 1> supportedResourceTypes = {
34     "Task"};
35 #endif
36 
37 static constexpr const uint8_t maxNoOfSubscriptions = 20;
38 
39 class EventService : public Node
40 {
41   public:
42     EventService(App& app) : Node(app, "/redfish/v1/EventService/")
43     {
44         entityPrivileges = {
45             {boost::beast::http::verb::get, {{"Login"}}},
46             {boost::beast::http::verb::head, {{"Login"}}},
47             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
48             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
49             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
50             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
51     }
52 
53   private:
54     void doGet(crow::Response& res, const crow::Request&,
55                const std::vector<std::string>&) override
56     {
57         auto asyncResp = std::make_shared<AsyncResp>(res);
58         res.jsonValue = {
59             {"@odata.type", "#EventService.v1_5_0.EventService"},
60             {"Id", "EventService"},
61             {"Name", "Event Service"},
62             {"Subscriptions",
63              {{"@odata.id", "/redfish/v1/EventService/Subscriptions"}}},
64             {"Actions",
65              {{"#EventService.SubmitTestEvent",
66                {{"target", "/redfish/v1/EventService/Actions/"
67                            "EventService.SubmitTestEvent"}}}}},
68             {"@odata.id", "/redfish/v1/EventService"}};
69 
70         const auto& [enabled, retryAttempts, retryTimeoutInterval] =
71             EventServiceManager::getInstance().getEventServiceConfig();
72 
73         asyncResp->res.jsonValue["Status"]["State"] =
74             (enabled ? "Enabled" : "Disabled");
75         asyncResp->res.jsonValue["ServiceEnabled"] = enabled;
76         asyncResp->res.jsonValue["DeliveryRetryAttempts"] = retryAttempts;
77         asyncResp->res.jsonValue["DeliveryRetryIntervalSeconds"] =
78             retryTimeoutInterval;
79         asyncResp->res.jsonValue["EventFormatTypes"] = supportedEvtFormatTypes;
80         asyncResp->res.jsonValue["RegistryPrefixes"] = supportedRegPrefixes;
81         asyncResp->res.jsonValue["ResourceTypes"] = supportedResourceTypes;
82 
83         nlohmann::json supportedSSEFilters = {
84             {"EventFormatType", true},        {"MessageId", true},
85             {"MetricReportDefinition", true}, {"RegistryPrefix", true},
86             {"OriginResource", false},        {"ResourceType", false}};
87 
88         asyncResp->res.jsonValue["SSEFilterPropertiesSupported"] =
89             supportedSSEFilters;
90     }
91 
92     void doPatch(crow::Response& res, const crow::Request& req,
93                  const std::vector<std::string>&) override
94     {
95         auto asyncResp = std::make_shared<AsyncResp>(res);
96 
97         std::optional<bool> serviceEnabled;
98         std::optional<uint32_t> retryAttemps;
99         std::optional<uint32_t> retryInterval;
100 
101         if (!json_util::readJson(req, res, "ServiceEnabled", serviceEnabled,
102                                  "DeliveryRetryAttempts", retryAttemps,
103                                  "DeliveryRetryIntervalSeconds", retryInterval))
104         {
105             return;
106         }
107 
108         auto [enabled, retryCount, retryTimeoutInterval] =
109             EventServiceManager::getInstance().getEventServiceConfig();
110 
111         if (serviceEnabled)
112         {
113             enabled = *serviceEnabled;
114         }
115 
116         if (retryAttemps)
117         {
118             // Supported range [1-3]
119             if ((*retryAttemps < 1) || (*retryAttemps > 3))
120             {
121                 messages::queryParameterOutOfRange(
122                     asyncResp->res, std::to_string(*retryAttemps),
123                     "DeliveryRetryAttempts", "[1-3]");
124             }
125             else
126             {
127                 retryCount = *retryAttemps;
128             }
129         }
130 
131         if (retryInterval)
132         {
133             // Supported range [30 - 180]
134             if ((*retryInterval < 30) || (*retryInterval > 180))
135             {
136                 messages::queryParameterOutOfRange(
137                     asyncResp->res, std::to_string(*retryInterval),
138                     "DeliveryRetryIntervalSeconds", "[30-180]");
139             }
140             else
141             {
142                 retryTimeoutInterval = *retryInterval;
143             }
144         }
145 
146         EventServiceManager::getInstance().setEventServiceConfig(
147             std::make_tuple(enabled, retryCount, retryTimeoutInterval));
148     }
149 };
150 
151 class SubmitTestEvent : public Node
152 {
153   public:
154     SubmitTestEvent(App& app) :
155         Node(app,
156              "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent/")
157     {
158         entityPrivileges = {
159             {boost::beast::http::verb::get, {{"Login"}}},
160             {boost::beast::http::verb::head, {{"Login"}}},
161             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
162             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
163             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
164             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
165     }
166 
167   private:
168     void doPost(crow::Response& res, const crow::Request&,
169                 const std::vector<std::string>&) override
170     {
171         EventServiceManager::getInstance().sendTestEventLog();
172         res.result(boost::beast::http::status::no_content);
173         res.end();
174     }
175 };
176 
177 class EventDestinationCollection : public Node
178 {
179   public:
180     EventDestinationCollection(App& app) :
181         Node(app, "/redfish/v1/EventService/Subscriptions/")
182     {
183         entityPrivileges = {
184             {boost::beast::http::verb::get, {{"Login"}}},
185             {boost::beast::http::verb::head, {{"Login"}}},
186             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
187             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
188             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
189             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
190     }
191 
192   private:
193     void doGet(crow::Response& res, const crow::Request&,
194                const std::vector<std::string>&) override
195     {
196         auto asyncResp = std::make_shared<AsyncResp>(res);
197 
198         res.jsonValue = {
199             {"@odata.type",
200              "#EventDestinationCollection.EventDestinationCollection"},
201             {"@odata.id", "/redfish/v1/EventService/Subscriptions"},
202             {"Name", "Event Destination Collections"}};
203 
204         nlohmann::json& memberArray = asyncResp->res.jsonValue["Members"];
205 
206         std::vector<std::string> subscripIds =
207             EventServiceManager::getInstance().getAllIDs();
208         memberArray = nlohmann::json::array();
209         asyncResp->res.jsonValue["Members@odata.count"] = subscripIds.size();
210 
211         for (const std::string& id : subscripIds)
212         {
213             memberArray.push_back(
214                 {{"@odata.id",
215                   "/redfish/v1/EventService/Subscriptions/" + id}});
216         }
217     }
218 
219     void doPost(crow::Response& res, const crow::Request& req,
220                 const std::vector<std::string>&) override
221     {
222         auto asyncResp = std::make_shared<AsyncResp>(res);
223 
224         if (EventServiceManager::getInstance().getNumberOfSubscriptions() >=
225             maxNoOfSubscriptions)
226         {
227             messages::eventSubscriptionLimitExceeded(asyncResp->res);
228             return;
229         }
230         std::string destUrl;
231         std::string protocol;
232         std::optional<std::string> context;
233         std::optional<std::string> subscriptionType;
234         std::optional<std::string> eventFormatType2;
235         std::optional<std::string> retryPolicy;
236         std::optional<std::vector<std::string>> msgIds;
237         std::optional<std::vector<std::string>> regPrefixes;
238         std::optional<std::vector<std::string>> resTypes;
239         std::optional<std::vector<nlohmann::json>> headers;
240         std::optional<std::vector<nlohmann::json>> mrdJsonArray;
241 
242         if (!json_util::readJson(
243                 req, res, "Destination", destUrl, "Context", context,
244                 "Protocol", protocol, "SubscriptionType", subscriptionType,
245                 "EventFormatType", eventFormatType2, "HttpHeaders", headers,
246                 "RegistryPrefixes", regPrefixes, "MessageIds", msgIds,
247                 "DeliveryRetryPolicy", retryPolicy, "MetricReportDefinitions",
248                 mrdJsonArray, "ResourceTypes", resTypes))
249         {
250             return;
251         }
252 
253         if (regPrefixes && msgIds)
254         {
255             if (regPrefixes->size() && msgIds->size())
256             {
257                 messages::mutualExclusiveProperties(
258                     asyncResp->res, "RegistryPrefixes", "MessageIds");
259                 return;
260             }
261         }
262 
263         // Validate the URL using regex expression
264         // Format: <protocol>://<host>:<port>/<uri>
265         // protocol: http/https
266         // host: Exclude ' ', ':', '#', '?'
267         // port: Empty or numeric value with ':' separator.
268         // uri: Start with '/' and Exclude '#', ' '
269         //      Can include query params(ex: '/event?test=1')
270         // TODO: Need to validate hostname extensively(as per rfc)
271         const std::regex urlRegex(
272             "(http|https)://([^/\\x20\\x3f\\x23\\x3a]+):?([0-9]*)(/"
273             "([^\\x20\\x23\\x3f]*\\x3f?([^\\x20\\x23\\x3f])*)?)");
274         std::cmatch match;
275         if (!std::regex_match(destUrl.c_str(), match, urlRegex))
276         {
277             messages::propertyValueFormatError(asyncResp->res, destUrl,
278                                                "Destination");
279             return;
280         }
281 
282         std::string uriProto = std::string(match[1].first, match[1].second);
283         if (uriProto == "http")
284         {
285 #ifndef BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING
286             messages::propertyValueFormatError(asyncResp->res, destUrl,
287                                                "Destination");
288             return;
289 #endif
290         }
291 
292         std::string host = std::string(match[2].first, match[2].second);
293         std::string port = std::string(match[3].first, match[3].second);
294         std::string path = std::string(match[4].first, match[4].second);
295         if (port.empty())
296         {
297             if (uriProto == "http")
298             {
299                 port = "80";
300             }
301             else
302             {
303                 port = "443";
304             }
305         }
306         if (path.empty())
307         {
308             path = "/";
309         }
310 
311         std::shared_ptr<Subscription> subValue =
312             std::make_shared<Subscription>(host, port, path, uriProto);
313 
314         subValue->destinationUrl = destUrl;
315 
316         if (subscriptionType)
317         {
318             if (*subscriptionType != "RedfishEvent")
319             {
320                 messages::propertyValueNotInList(
321                     asyncResp->res, *subscriptionType, "SubscriptionType");
322                 return;
323             }
324             subValue->subscriptionType = *subscriptionType;
325         }
326         else
327         {
328             subValue->subscriptionType = "RedfishEvent"; // Default
329         }
330 
331         if (protocol != "Redfish")
332         {
333             messages::propertyValueNotInList(asyncResp->res, protocol,
334                                              "Protocol");
335             return;
336         }
337         subValue->protocol = protocol;
338 
339         if (eventFormatType2)
340         {
341             if (std::find(supportedEvtFormatTypes.begin(),
342                           supportedEvtFormatTypes.end(),
343                           *eventFormatType2) == supportedEvtFormatTypes.end())
344             {
345                 messages::propertyValueNotInList(
346                     asyncResp->res, *eventFormatType2, "EventFormatType");
347                 return;
348             }
349             subValue->eventFormatType = *eventFormatType2;
350         }
351         else
352         {
353             // If not specified, use default "Event"
354             subValue->eventFormatType = "Event";
355         }
356 
357         if (context)
358         {
359             subValue->customText = *context;
360         }
361 
362         if (headers)
363         {
364             subValue->httpHeaders = *headers;
365         }
366 
367         if (regPrefixes)
368         {
369             for (const std::string& it : *regPrefixes)
370             {
371                 if (std::find(supportedRegPrefixes.begin(),
372                               supportedRegPrefixes.end(),
373                               it) == supportedRegPrefixes.end())
374                 {
375                     messages::propertyValueNotInList(asyncResp->res, it,
376                                                      "RegistryPrefixes");
377                     return;
378                 }
379             }
380             subValue->registryPrefixes = *regPrefixes;
381         }
382 
383         if (resTypes)
384         {
385             for (const std::string& it : *resTypes)
386             {
387                 if (std::find(supportedResourceTypes.begin(),
388                               supportedResourceTypes.end(),
389                               it) == supportedResourceTypes.end())
390                 {
391                     messages::propertyValueNotInList(asyncResp->res, it,
392                                                      "ResourceTypes");
393                     return;
394                 }
395             }
396             subValue->resourceTypes = *resTypes;
397         }
398 
399         if (msgIds)
400         {
401             // Do we need to loop-up MessageRegistry and validate
402             // data for authenticity??? Not mandate, i believe.
403             subValue->registryMsgIds = *msgIds;
404         }
405 
406         if (retryPolicy)
407         {
408             if (std::find(supportedRetryPolicies.begin(),
409                           supportedRetryPolicies.end(),
410                           *retryPolicy) == supportedRetryPolicies.end())
411             {
412                 messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
413                                                  "DeliveryRetryPolicy");
414                 return;
415             }
416             subValue->retryPolicy = *retryPolicy;
417         }
418         else
419         {
420             // Default "TerminateAfterRetries"
421             subValue->retryPolicy = "TerminateAfterRetries";
422         }
423 
424         if (mrdJsonArray)
425         {
426             for (nlohmann::json& mrdObj : *mrdJsonArray)
427             {
428                 std::string mrdUri;
429                 if (json_util::getValueFromJsonObject(mrdObj, "@odata.id",
430                                                       mrdUri))
431                 {
432                     subValue->metricReportDefinitions.emplace_back(mrdUri);
433                 }
434                 else
435                 {
436                     messages::propertyValueFormatError(
437                         asyncResp->res, mrdObj.dump(),
438                         "MetricReportDefinitions");
439                     return;
440                 }
441             }
442         }
443 
444         std::string id =
445             EventServiceManager::getInstance().addSubscription(subValue);
446         if (id.empty())
447         {
448             messages::internalError(asyncResp->res);
449             return;
450         }
451 
452         messages::created(asyncResp->res);
453         asyncResp->res.addHeader(
454             "Location", "/redfish/v1/EventService/Subscriptions/" + id);
455     }
456 };
457 
458 class EventDestination : public Node
459 {
460   public:
461     EventDestination(App& app) :
462         Node(app, "/redfish/v1/EventService/Subscriptions/<str>/",
463              std::string())
464     {
465         entityPrivileges = {
466             {boost::beast::http::verb::get, {{"Login"}}},
467             {boost::beast::http::verb::head, {{"Login"}}},
468             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
469             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
470             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
471             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
472     }
473 
474   private:
475     void doGet(crow::Response& res, const crow::Request&,
476                const std::vector<std::string>& params) override
477     {
478         auto asyncResp = std::make_shared<AsyncResp>(res);
479         if (params.size() != 1)
480         {
481             messages::internalError(asyncResp->res);
482             return;
483         }
484 
485         std::shared_ptr<Subscription> subValue =
486             EventServiceManager::getInstance().getSubscription(params[0]);
487         if (subValue == nullptr)
488         {
489             res.result(boost::beast::http::status::not_found);
490             res.end();
491             return;
492         }
493         const std::string& id = params[0];
494 
495         res.jsonValue = {
496             {"@odata.type", "#EventDestination.v1_7_0.EventDestination"},
497             {"Protocol", "Redfish"}};
498         asyncResp->res.jsonValue["@odata.id"] =
499             "/redfish/v1/EventService/Subscriptions/" + id;
500         asyncResp->res.jsonValue["Id"] = id;
501         asyncResp->res.jsonValue["Name"] = "Event Destination " + id;
502         asyncResp->res.jsonValue["Destination"] = subValue->destinationUrl;
503         asyncResp->res.jsonValue["Context"] = subValue->customText;
504         asyncResp->res.jsonValue["SubscriptionType"] =
505             subValue->subscriptionType;
506         asyncResp->res.jsonValue["HttpHeaders"] = subValue->httpHeaders;
507         asyncResp->res.jsonValue["EventFormatType"] = subValue->eventFormatType;
508         asyncResp->res.jsonValue["RegistryPrefixes"] =
509             subValue->registryPrefixes;
510         asyncResp->res.jsonValue["ResourceTypes"] = subValue->resourceTypes;
511 
512         asyncResp->res.jsonValue["MessageIds"] = subValue->registryMsgIds;
513         asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue->retryPolicy;
514 
515         std::vector<nlohmann::json> mrdJsonArray;
516         for (const auto& mdrUri : subValue->metricReportDefinitions)
517         {
518             mrdJsonArray.push_back({{"@odata.id", mdrUri}});
519         }
520         asyncResp->res.jsonValue["MetricReportDefinitions"] = mrdJsonArray;
521     }
522 
523     void doPatch(crow::Response& res, const crow::Request& req,
524                  const std::vector<std::string>& params) override
525     {
526         auto asyncResp = std::make_shared<AsyncResp>(res);
527         if (params.size() != 1)
528         {
529             messages::internalError(asyncResp->res);
530             return;
531         }
532 
533         std::shared_ptr<Subscription> subValue =
534             EventServiceManager::getInstance().getSubscription(params[0]);
535         if (subValue == nullptr)
536         {
537             res.result(boost::beast::http::status::not_found);
538             res.end();
539             return;
540         }
541 
542         std::optional<std::string> context;
543         std::optional<std::string> retryPolicy;
544         std::optional<std::vector<nlohmann::json>> headers;
545 
546         if (!json_util::readJson(req, res, "Context", context,
547                                  "DeliveryRetryPolicy", retryPolicy,
548                                  "HttpHeaders", headers))
549         {
550             return;
551         }
552 
553         if (context)
554         {
555             subValue->customText = *context;
556         }
557 
558         if (headers)
559         {
560             subValue->httpHeaders = *headers;
561         }
562 
563         if (retryPolicy)
564         {
565             if (std::find(supportedRetryPolicies.begin(),
566                           supportedRetryPolicies.end(),
567                           *retryPolicy) == supportedRetryPolicies.end())
568             {
569                 messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
570                                                  "DeliveryRetryPolicy");
571                 return;
572             }
573             subValue->retryPolicy = *retryPolicy;
574             subValue->updateRetryPolicy();
575         }
576 
577         EventServiceManager::getInstance().updateSubscriptionData();
578     }
579 
580     void doDelete(crow::Response& res, const crow::Request&,
581                   const std::vector<std::string>& params) override
582     {
583         auto asyncResp = std::make_shared<AsyncResp>(res);
584 
585         if (params.size() != 1)
586         {
587             messages::internalError(asyncResp->res);
588             return;
589         }
590 
591         if (!EventServiceManager::getInstance().isSubscriptionExist(params[0]))
592         {
593             res.result(boost::beast::http::status::not_found);
594             res.end();
595             return;
596         }
597         EventServiceManager::getInstance().deleteSubscription(params[0]);
598     }
599 };
600 
601 } // namespace redfish
602