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