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", "TaskEvent"};
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(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
55                const crow::Request&, const std::vector<std::string>&) override
56     {
57 
58         asyncResp->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(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
93                  const crow::Request& req,
94                  const std::vector<std::string>&) override
95     {
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, asyncResp->res, "ServiceEnabled",
102                                  serviceEnabled, "DeliveryRetryAttempts",
103                                  retryAttemps, "DeliveryRetryIntervalSeconds",
104                                  retryInterval))
105         {
106             return;
107         }
108 
109         auto [enabled, retryCount, retryTimeoutInterval] =
110             EventServiceManager::getInstance().getEventServiceConfig();
111 
112         if (serviceEnabled)
113         {
114             enabled = *serviceEnabled;
115         }
116 
117         if (retryAttemps)
118         {
119             // Supported range [1-3]
120             if ((*retryAttemps < 1) || (*retryAttemps > 3))
121             {
122                 messages::queryParameterOutOfRange(
123                     asyncResp->res, std::to_string(*retryAttemps),
124                     "DeliveryRetryAttempts", "[1-3]");
125             }
126             else
127             {
128                 retryCount = *retryAttemps;
129             }
130         }
131 
132         if (retryInterval)
133         {
134             // Supported range [30 - 180]
135             if ((*retryInterval < 30) || (*retryInterval > 180))
136             {
137                 messages::queryParameterOutOfRange(
138                     asyncResp->res, std::to_string(*retryInterval),
139                     "DeliveryRetryIntervalSeconds", "[30-180]");
140             }
141             else
142             {
143                 retryTimeoutInterval = *retryInterval;
144             }
145         }
146 
147         EventServiceManager::getInstance().setEventServiceConfig(
148             std::make_tuple(enabled, retryCount, retryTimeoutInterval));
149     }
150 };
151 
152 class SubmitTestEvent : public Node
153 {
154   public:
155     SubmitTestEvent(App& app) :
156         Node(app,
157              "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent/")
158     {
159         entityPrivileges = {
160             {boost::beast::http::verb::get, {{"Login"}}},
161             {boost::beast::http::verb::head, {{"Login"}}},
162             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
163             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
164             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
165             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
166     }
167 
168   private:
169     void doPost(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
170                 const crow::Request&, const std::vector<std::string>&) override
171     {
172         EventServiceManager::getInstance().sendTestEventLog();
173         asyncResp->res.result(boost::beast::http::status::no_content);
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(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
194                const crow::Request&, const std::vector<std::string>&) override
195     {
196 
197         asyncResp->res.jsonValue = {
198             {"@odata.type",
199              "#EventDestinationCollection.EventDestinationCollection"},
200             {"@odata.id", "/redfish/v1/EventService/Subscriptions"},
201             {"Name", "Event Destination Collections"}};
202 
203         nlohmann::json& memberArray = asyncResp->res.jsonValue["Members"];
204 
205         std::vector<std::string> subscripIds =
206             EventServiceManager::getInstance().getAllIDs();
207         memberArray = nlohmann::json::array();
208         asyncResp->res.jsonValue["Members@odata.count"] = subscripIds.size();
209 
210         for (const std::string& id : subscripIds)
211         {
212             memberArray.push_back(
213                 {{"@odata.id",
214                   "/redfish/v1/EventService/Subscriptions/" + id}});
215         }
216     }
217 
218     void doPost(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
219                 const crow::Request& req,
220                 const std::vector<std::string>&) override
221     {
222 
223         if (EventServiceManager::getInstance().getNumberOfSubscriptions() >=
224             maxNoOfSubscriptions)
225         {
226             messages::eventSubscriptionLimitExceeded(asyncResp->res);
227             return;
228         }
229         std::string destUrl;
230         std::string protocol;
231         std::optional<std::string> context;
232         std::optional<std::string> subscriptionType;
233         std::optional<std::string> eventFormatType2;
234         std::optional<std::string> retryPolicy;
235         std::optional<std::vector<std::string>> msgIds;
236         std::optional<std::vector<std::string>> regPrefixes;
237         std::optional<std::vector<std::string>> resTypes;
238         std::optional<std::vector<nlohmann::json>> headers;
239         std::optional<std::vector<nlohmann::json>> mrdJsonArray;
240 
241         if (!json_util::readJson(
242                 req, asyncResp->res, "Destination", destUrl, "Context", context,
243                 "Protocol", protocol, "SubscriptionType", subscriptionType,
244                 "EventFormatType", eventFormatType2, "HttpHeaders", headers,
245                 "RegistryPrefixes", regPrefixes, "MessageIds", msgIds,
246                 "DeliveryRetryPolicy", retryPolicy, "MetricReportDefinitions",
247                 mrdJsonArray, "ResourceTypes", resTypes))
248         {
249             return;
250         }
251 
252         if (regPrefixes && msgIds)
253         {
254             if (regPrefixes->size() && msgIds->size())
255             {
256                 messages::mutualExclusiveProperties(
257                     asyncResp->res, "RegistryPrefixes", "MessageIds");
258                 return;
259             }
260         }
261 
262         // Validate the URL using regex expression
263         // Format: <protocol>://<host>:<port>/<uri>
264         // protocol: http/https
265         // host: Exclude ' ', ':', '#', '?'
266         // port: Empty or numeric value with ':' separator.
267         // uri: Start with '/' and Exclude '#', ' '
268         //      Can include query params(ex: '/event?test=1')
269         // TODO: Need to validate hostname extensively(as per rfc)
270         const std::regex urlRegex(
271             "(http|https)://([^/\\x20\\x3f\\x23\\x3a]+):?([0-9]*)(/"
272             "([^\\x20\\x23\\x3f]*\\x3f?([^\\x20\\x23\\x3f])*)?)");
273         std::cmatch match;
274         if (!std::regex_match(destUrl.c_str(), match, urlRegex))
275         {
276             messages::propertyValueFormatError(asyncResp->res, destUrl,
277                                                "Destination");
278             return;
279         }
280 
281         std::string uriProto = std::string(match[1].first, match[1].second);
282         if (uriProto == "http")
283         {
284 #ifndef BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING
285             messages::propertyValueFormatError(asyncResp->res, destUrl,
286                                                "Destination");
287             return;
288 #endif
289         }
290 
291         std::string host = std::string(match[2].first, match[2].second);
292         std::string port = std::string(match[3].first, match[3].second);
293         std::string path = std::string(match[4].first, match[4].second);
294         if (port.empty())
295         {
296             if (uriProto == "http")
297             {
298                 port = "80";
299             }
300             else
301             {
302                 port = "443";
303             }
304         }
305         if (path.empty())
306         {
307             path = "/";
308         }
309 
310         std::shared_ptr<Subscription> subValue =
311             std::make_shared<Subscription>(host, port, path, uriProto);
312 
313         subValue->destinationUrl = destUrl;
314 
315         if (subscriptionType)
316         {
317             if (*subscriptionType != "RedfishEvent")
318             {
319                 messages::propertyValueNotInList(
320                     asyncResp->res, *subscriptionType, "SubscriptionType");
321                 return;
322             }
323             subValue->subscriptionType = *subscriptionType;
324         }
325         else
326         {
327             subValue->subscriptionType = "RedfishEvent"; // Default
328         }
329 
330         if (protocol != "Redfish")
331         {
332             messages::propertyValueNotInList(asyncResp->res, protocol,
333                                              "Protocol");
334             return;
335         }
336         subValue->protocol = protocol;
337 
338         if (eventFormatType2)
339         {
340             if (std::find(supportedEvtFormatTypes.begin(),
341                           supportedEvtFormatTypes.end(),
342                           *eventFormatType2) == supportedEvtFormatTypes.end())
343             {
344                 messages::propertyValueNotInList(
345                     asyncResp->res, *eventFormatType2, "EventFormatType");
346                 return;
347             }
348             subValue->eventFormatType = *eventFormatType2;
349         }
350         else
351         {
352             // If not specified, use default "Event"
353             subValue->eventFormatType = "Event";
354         }
355 
356         if (context)
357         {
358             subValue->customText = *context;
359         }
360 
361         if (headers)
362         {
363             subValue->httpHeaders = *headers;
364         }
365 
366         if (regPrefixes)
367         {
368             for (const std::string& it : *regPrefixes)
369             {
370                 if (std::find(supportedRegPrefixes.begin(),
371                               supportedRegPrefixes.end(),
372                               it) == supportedRegPrefixes.end())
373                 {
374                     messages::propertyValueNotInList(asyncResp->res, it,
375                                                      "RegistryPrefixes");
376                     return;
377                 }
378             }
379             subValue->registryPrefixes = *regPrefixes;
380         }
381 
382         if (resTypes)
383         {
384             for (const std::string& it : *resTypes)
385             {
386                 if (std::find(supportedResourceTypes.begin(),
387                               supportedResourceTypes.end(),
388                               it) == supportedResourceTypes.end())
389                 {
390                     messages::propertyValueNotInList(asyncResp->res, it,
391                                                      "ResourceTypes");
392                     return;
393                 }
394             }
395             subValue->resourceTypes = *resTypes;
396         }
397 
398         if (msgIds)
399         {
400             std::vector<std::string> registryPrefix;
401 
402             // If no registry prefixes are mentioned, consider all supported
403             // prefixes
404             if (subValue->registryPrefixes.empty())
405             {
406                 registryPrefix.assign(supportedRegPrefixes.begin(),
407                                       supportedRegPrefixes.end());
408             }
409             else
410             {
411                 registryPrefix = subValue->registryPrefixes;
412             }
413 
414             for (const std::string& id : *msgIds)
415             {
416                 bool validId = false;
417 
418                 // Check for Message ID in each of the selected Registry
419                 for (const std::string& it : registryPrefix)
420                 {
421                     const boost::beast::span<
422                         const redfish::message_registries::MessageEntry>
423                         registry =
424                             redfish::message_registries::getRegistryFromPrefix(
425                                 it);
426 
427                     if (std::any_of(
428                             registry.cbegin(), registry.cend(),
429                             [&id](
430                                 const redfish::message_registries::MessageEntry&
431                                     messageEntry) {
432                                 return !id.compare(messageEntry.first);
433                             }))
434                     {
435                         validId = true;
436                         break;
437                     }
438                 }
439 
440                 if (!validId)
441                 {
442                     messages::propertyValueNotInList(asyncResp->res, id,
443                                                      "MessageIds");
444                     return;
445                 }
446             }
447 
448             subValue->registryMsgIds = *msgIds;
449         }
450 
451         if (retryPolicy)
452         {
453             if (std::find(supportedRetryPolicies.begin(),
454                           supportedRetryPolicies.end(),
455                           *retryPolicy) == supportedRetryPolicies.end())
456             {
457                 messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
458                                                  "DeliveryRetryPolicy");
459                 return;
460             }
461             subValue->retryPolicy = *retryPolicy;
462         }
463         else
464         {
465             // Default "TerminateAfterRetries"
466             subValue->retryPolicy = "TerminateAfterRetries";
467         }
468 
469         if (mrdJsonArray)
470         {
471             for (nlohmann::json& mrdObj : *mrdJsonArray)
472             {
473                 std::string mrdUri;
474                 if (json_util::getValueFromJsonObject(mrdObj, "@odata.id",
475                                                       mrdUri))
476                 {
477                     subValue->metricReportDefinitions.emplace_back(mrdUri);
478                 }
479                 else
480                 {
481                     messages::propertyValueFormatError(
482                         asyncResp->res,
483                         mrdObj.dump(2, ' ', true,
484                                     nlohmann::json::error_handler_t::replace),
485                         "MetricReportDefinitions");
486                     return;
487                 }
488             }
489         }
490 
491         std::string id =
492             EventServiceManager::getInstance().addSubscription(subValue);
493         if (id.empty())
494         {
495             messages::internalError(asyncResp->res);
496             return;
497         }
498 
499         messages::created(asyncResp->res);
500         asyncResp->res.addHeader(
501             "Location", "/redfish/v1/EventService/Subscriptions/" + id);
502     }
503 };
504 
505 class EventDestination : public Node
506 {
507   public:
508     EventDestination(App& app) :
509         Node(app, "/redfish/v1/EventService/Subscriptions/<str>/",
510              std::string())
511     {
512         entityPrivileges = {
513             {boost::beast::http::verb::get, {{"Login"}}},
514             {boost::beast::http::verb::head, {{"Login"}}},
515             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
516             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
517             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
518             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
519     }
520 
521   private:
522     void doGet(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
523                const crow::Request&,
524                const std::vector<std::string>& params) override
525     {
526 
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             asyncResp->res.result(boost::beast::http::status::not_found);
538             return;
539         }
540         const std::string& id = params[0];
541 
542         asyncResp->res.jsonValue = {
543             {"@odata.type", "#EventDestination.v1_7_0.EventDestination"},
544             {"Protocol", "Redfish"}};
545         asyncResp->res.jsonValue["@odata.id"] =
546             "/redfish/v1/EventService/Subscriptions/" + id;
547         asyncResp->res.jsonValue["Id"] = id;
548         asyncResp->res.jsonValue["Name"] = "Event Destination " + id;
549         asyncResp->res.jsonValue["Destination"] = subValue->destinationUrl;
550         asyncResp->res.jsonValue["Context"] = subValue->customText;
551         asyncResp->res.jsonValue["SubscriptionType"] =
552             subValue->subscriptionType;
553         asyncResp->res.jsonValue["HttpHeaders"] = subValue->httpHeaders;
554         asyncResp->res.jsonValue["EventFormatType"] = subValue->eventFormatType;
555         asyncResp->res.jsonValue["RegistryPrefixes"] =
556             subValue->registryPrefixes;
557         asyncResp->res.jsonValue["ResourceTypes"] = subValue->resourceTypes;
558 
559         asyncResp->res.jsonValue["MessageIds"] = subValue->registryMsgIds;
560         asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue->retryPolicy;
561 
562         std::vector<nlohmann::json> mrdJsonArray;
563         for (const auto& mdrUri : subValue->metricReportDefinitions)
564         {
565             mrdJsonArray.push_back({{"@odata.id", mdrUri}});
566         }
567         asyncResp->res.jsonValue["MetricReportDefinitions"] = mrdJsonArray;
568     }
569 
570     void doPatch(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
571                  const crow::Request& req,
572                  const std::vector<std::string>& params) override
573     {
574 
575         if (params.size() != 1)
576         {
577             messages::internalError(asyncResp->res);
578             return;
579         }
580 
581         std::shared_ptr<Subscription> subValue =
582             EventServiceManager::getInstance().getSubscription(params[0]);
583         if (subValue == nullptr)
584         {
585             asyncResp->res.result(boost::beast::http::status::not_found);
586             return;
587         }
588 
589         std::optional<std::string> context;
590         std::optional<std::string> retryPolicy;
591         std::optional<std::vector<nlohmann::json>> headers;
592 
593         if (!json_util::readJson(req, asyncResp->res, "Context", context,
594                                  "DeliveryRetryPolicy", retryPolicy,
595                                  "HttpHeaders", headers))
596         {
597             return;
598         }
599 
600         if (context)
601         {
602             subValue->customText = *context;
603         }
604 
605         if (headers)
606         {
607             subValue->httpHeaders = *headers;
608         }
609 
610         if (retryPolicy)
611         {
612             if (std::find(supportedRetryPolicies.begin(),
613                           supportedRetryPolicies.end(),
614                           *retryPolicy) == supportedRetryPolicies.end())
615             {
616                 messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
617                                                  "DeliveryRetryPolicy");
618                 return;
619             }
620             subValue->retryPolicy = *retryPolicy;
621             subValue->updateRetryPolicy();
622         }
623 
624         EventServiceManager::getInstance().updateSubscriptionData();
625     }
626 
627     void doDelete(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
628                   const crow::Request&,
629                   const std::vector<std::string>& params) override
630     {
631 
632         if (params.size() != 1)
633         {
634             messages::internalError(asyncResp->res);
635             return;
636         }
637 
638         if (!EventServiceManager::getInstance().isSubscriptionExist(params[0]))
639         {
640             asyncResp->res.result(boost::beast::http::status::not_found);
641             return;
642         }
643         EventServiceManager::getInstance().deleteSubscription(params[0]);
644     }
645 };
646 
647 } // namespace redfish
648