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