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