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 EventDestination : public Node
402 {
403   public:
404     EventDestination(CrowApp& app) :
405         Node(app, "/redfish/v1/EventService/Subscriptions/<str>/",
406              std::string())
407     {
408         entityPrivileges = {
409             {boost::beast::http::verb::get, {{"Login"}}},
410             {boost::beast::http::verb::head, {{"Login"}}},
411             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
412             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
413             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
414             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
415     }
416 
417   private:
418     void doGet(crow::Response& res, const crow::Request& req,
419                const std::vector<std::string>& params) override
420     {
421         auto asyncResp = std::make_shared<AsyncResp>(res);
422         if (params.size() != 1)
423         {
424             messages::internalError(asyncResp->res);
425             return;
426         }
427 
428         std::shared_ptr<Subscription> subValue =
429             EventServiceManager::getInstance().getSubscription(params[0]);
430         if (subValue == nullptr)
431         {
432             res.result(boost::beast::http::status::not_found);
433             res.end();
434             return;
435         }
436         const std::string& id = params[0];
437 
438         res.jsonValue = {
439             {"@odata.type", "#EventDestination.v1_7_0.EventDestination"},
440             {"Protocol", "Redfish"}};
441         asyncResp->res.jsonValue["@odata.id"] =
442             "/redfish/v1/EventService/Subscriptions/" + id;
443         asyncResp->res.jsonValue["Id"] = id;
444         asyncResp->res.jsonValue["Name"] = "Event Destination " + id;
445         asyncResp->res.jsonValue["Destination"] = subValue->destinationUrl;
446         asyncResp->res.jsonValue["Context"] = subValue->customText;
447         asyncResp->res.jsonValue["SubscriptionType"] =
448             subValue->subscriptionType;
449         asyncResp->res.jsonValue["HttpHeaders"] = subValue->httpHeaders;
450         asyncResp->res.jsonValue["EventFormatType"] = subValue->eventFormatType;
451         asyncResp->res.jsonValue["RegistryPrefixes"] =
452             subValue->registryPrefixes;
453         asyncResp->res.jsonValue["MessageIds"] = subValue->registryMsgIds;
454         asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue->retryPolicy;
455         asyncResp->res.jsonValue["MetricReportDefinitions"] =
456             subValue->metricReportDefinitions;
457     }
458 
459     void doPatch(crow::Response& res, const crow::Request& req,
460                  const std::vector<std::string>& params) override
461     {
462         auto asyncResp = std::make_shared<AsyncResp>(res);
463         if (params.size() != 1)
464         {
465             messages::internalError(asyncResp->res);
466             return;
467         }
468 
469         std::shared_ptr<Subscription> subValue =
470             EventServiceManager::getInstance().getSubscription(params[0]);
471         if (subValue == nullptr)
472         {
473             res.result(boost::beast::http::status::not_found);
474             res.end();
475             return;
476         }
477 
478         std::optional<std::string> context;
479         std::optional<std::string> retryPolicy;
480         std::optional<std::vector<nlohmann::json>> headers;
481 
482         if (!json_util::readJson(req, res, "Context", context,
483                                  "DeliveryRetryPolicy", retryPolicy,
484                                  "HttpHeaders", headers))
485         {
486             return;
487         }
488 
489         if (context)
490         {
491             subValue->customText = *context;
492         }
493 
494         if (headers)
495         {
496             subValue->httpHeaders = *headers;
497         }
498 
499         if (retryPolicy)
500         {
501             if (std::find(supportedRetryPolicies.begin(),
502                           supportedRetryPolicies.end(),
503                           *retryPolicy) == supportedRetryPolicies.end())
504             {
505                 messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
506                                                  "DeliveryRetryPolicy");
507                 return;
508             }
509             subValue->retryPolicy = *retryPolicy;
510         }
511 
512         EventServiceManager::getInstance().updateSubscriptionData();
513     }
514 
515     void doDelete(crow::Response& res, const crow::Request& req,
516                   const std::vector<std::string>& params) override
517     {
518         auto asyncResp = std::make_shared<AsyncResp>(res);
519 
520         if (params.size() != 1)
521         {
522             messages::internalError(asyncResp->res);
523             return;
524         }
525 
526         if (!EventServiceManager::getInstance().isSubscriptionExist(params[0]))
527         {
528             res.result(boost::beast::http::status::not_found);
529             res.end();
530             return;
531         }
532         EventServiceManager::getInstance().deleteSubscription(params[0]);
533     }
534 };
535 
536 } // namespace redfish
537