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