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