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