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(CrowApp& 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(CrowApp& 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(CrowApp& 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> eventFormatType; 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>> metricReportDefinitions; 243 244 if (!json_util::readJson( 245 req, res, "Destination", destUrl, "Context", context, 246 "Protocol", protocol, "SubscriptionType", subscriptionType, 247 "EventFormatType", eventFormatType, "HttpHeaders", headers, 248 "RegistryPrefixes", regPrefixes, "MessageIds", msgIds, 249 "DeliveryRetryPolicy", retryPolicy, "MetricReportDefinitions", 250 metricReportDefinitions, "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 (eventFormatType) 332 { 333 if (std::find(supportedEvtFormatTypes.begin(), 334 supportedEvtFormatTypes.end(), 335 *eventFormatType) == supportedEvtFormatTypes.end()) 336 { 337 messages::propertyValueNotInList( 338 asyncResp->res, *eventFormatType, "EventFormatType"); 339 return; 340 } 341 subValue->eventFormatType = *eventFormatType; 342 } 343 else 344 { 345 // If not specified, use default "Event" 346 subValue->eventFormatType.assign({"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 (metricReportDefinitions) 417 { 418 subValue->metricReportDefinitions = *metricReportDefinitions; 419 } 420 421 std::string id = 422 EventServiceManager::getInstance().addSubscription(subValue); 423 if (id.empty()) 424 { 425 messages::internalError(asyncResp->res); 426 return; 427 } 428 429 messages::created(asyncResp->res); 430 asyncResp->res.addHeader( 431 "Location", "/redfish/v1/EventService/Subscriptions/" + id); 432 } 433 }; 434 435 class EventServiceSSE : public Node 436 { 437 public: 438 EventServiceSSE(CrowApp& app) : 439 Node(app, "/redfish/v1/EventService/Subscriptions/SSE/") 440 { 441 entityPrivileges = { 442 {boost::beast::http::verb::get, {{"ConfigureManager"}}}, 443 {boost::beast::http::verb::head, {{"ConfigureManager"}}}, 444 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 445 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 446 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 447 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 448 } 449 450 private: 451 void doGet(crow::Response& res, const crow::Request& req, 452 const std::vector<std::string>& params) override 453 { 454 if (EventServiceManager::getInstance().getNumberOfSubscriptions() >= 455 maxNoOfSubscriptions) 456 { 457 messages::eventSubscriptionLimitExceeded(res); 458 res.end(); 459 return; 460 } 461 462 std::shared_ptr<crow::Request::Adaptor> sseConn = 463 std::make_shared<crow::Request::Adaptor>(std::move(req.socket())); 464 std::shared_ptr<Subscription> subValue = 465 std::make_shared<Subscription>(sseConn); 466 467 // GET on this URI means, Its SSE subscriptionType. 468 subValue->subscriptionType = "SSE"; 469 470 // Default values 471 subValue->protocol = "Redfish"; 472 subValue->retryPolicy = "TerminateAfterRetries"; 473 474 boost::urls::url_view::params_type::iterator it = 475 req.urlParams.find("$filter"); 476 if (it == req.urlParams.end()) 477 { 478 subValue->eventFormatType = "Event"; 479 } 480 481 else 482 { 483 std::string filters = it->value(); 484 // Reading from query params. 485 bool status = readSSEQueryParams( 486 filters, subValue->eventFormatType, subValue->registryMsgIds, 487 subValue->registryPrefixes, subValue->metricReportDefinitions); 488 489 if (!status) 490 { 491 messages::invalidObject(res, filters); 492 return; 493 } 494 495 if (!subValue->eventFormatType.empty()) 496 { 497 if (std::find(supportedEvtFormatTypes.begin(), 498 supportedEvtFormatTypes.end(), 499 subValue->eventFormatType) == 500 supportedEvtFormatTypes.end()) 501 { 502 messages::propertyValueNotInList( 503 res, subValue->eventFormatType, "EventFormatType"); 504 return; 505 } 506 } 507 else 508 { 509 // If nothing specified, using default "Event" 510 subValue->eventFormatType.assign({"Event"}); 511 } 512 513 if (!subValue->registryPrefixes.empty()) 514 { 515 for (const std::string& it : subValue->registryPrefixes) 516 { 517 if (std::find(supportedRegPrefixes.begin(), 518 supportedRegPrefixes.end(), 519 it) == supportedRegPrefixes.end()) 520 { 521 messages::propertyValueNotInList(res, it, 522 "RegistryPrefixes"); 523 return; 524 } 525 } 526 } 527 } 528 529 std::string id = 530 EventServiceManager::getInstance().addSubscription(subValue, false); 531 if (id.empty()) 532 { 533 messages::internalError(res); 534 res.end(); 535 return; 536 } 537 } 538 }; 539 540 class EventDestination : public Node 541 { 542 public: 543 EventDestination(CrowApp& app) : 544 Node(app, "/redfish/v1/EventService/Subscriptions/<str>/", 545 std::string()) 546 { 547 entityPrivileges = { 548 {boost::beast::http::verb::get, {{"Login"}}}, 549 {boost::beast::http::verb::head, {{"Login"}}}, 550 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 551 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 552 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 553 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 554 } 555 556 private: 557 void doGet(crow::Response& res, const crow::Request& req, 558 const std::vector<std::string>& params) override 559 { 560 auto asyncResp = std::make_shared<AsyncResp>(res); 561 if (params.size() != 1) 562 { 563 messages::internalError(asyncResp->res); 564 return; 565 } 566 567 std::shared_ptr<Subscription> subValue = 568 EventServiceManager::getInstance().getSubscription(params[0]); 569 if (subValue == nullptr) 570 { 571 res.result(boost::beast::http::status::not_found); 572 res.end(); 573 return; 574 } 575 const std::string& id = params[0]; 576 577 res.jsonValue = { 578 {"@odata.type", "#EventDestination.v1_7_0.EventDestination"}, 579 {"Protocol", "Redfish"}}; 580 asyncResp->res.jsonValue["@odata.id"] = 581 "/redfish/v1/EventService/Subscriptions/" + id; 582 asyncResp->res.jsonValue["Id"] = id; 583 asyncResp->res.jsonValue["Name"] = "Event Destination " + id; 584 asyncResp->res.jsonValue["Destination"] = subValue->destinationUrl; 585 asyncResp->res.jsonValue["Context"] = subValue->customText; 586 asyncResp->res.jsonValue["SubscriptionType"] = 587 subValue->subscriptionType; 588 asyncResp->res.jsonValue["HttpHeaders"] = subValue->httpHeaders; 589 asyncResp->res.jsonValue["EventFormatType"] = subValue->eventFormatType; 590 asyncResp->res.jsonValue["RegistryPrefixes"] = 591 subValue->registryPrefixes; 592 asyncResp->res.jsonValue["ResourceTypes"] = subValue->resourceTypes; 593 594 asyncResp->res.jsonValue["MessageIds"] = subValue->registryMsgIds; 595 asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue->retryPolicy; 596 asyncResp->res.jsonValue["MetricReportDefinitions"] = 597 subValue->metricReportDefinitions; 598 } 599 600 void doPatch(crow::Response& res, const crow::Request& req, 601 const std::vector<std::string>& params) override 602 { 603 auto asyncResp = std::make_shared<AsyncResp>(res); 604 if (params.size() != 1) 605 { 606 messages::internalError(asyncResp->res); 607 return; 608 } 609 610 std::shared_ptr<Subscription> subValue = 611 EventServiceManager::getInstance().getSubscription(params[0]); 612 if (subValue == nullptr) 613 { 614 res.result(boost::beast::http::status::not_found); 615 res.end(); 616 return; 617 } 618 619 std::optional<std::string> context; 620 std::optional<std::string> retryPolicy; 621 std::optional<std::vector<nlohmann::json>> headers; 622 623 if (!json_util::readJson(req, res, "Context", context, 624 "DeliveryRetryPolicy", retryPolicy, 625 "HttpHeaders", headers)) 626 { 627 return; 628 } 629 630 if (context) 631 { 632 subValue->customText = *context; 633 } 634 635 if (headers) 636 { 637 subValue->httpHeaders = *headers; 638 } 639 640 if (retryPolicy) 641 { 642 if (std::find(supportedRetryPolicies.begin(), 643 supportedRetryPolicies.end(), 644 *retryPolicy) == supportedRetryPolicies.end()) 645 { 646 messages::propertyValueNotInList(asyncResp->res, *retryPolicy, 647 "DeliveryRetryPolicy"); 648 return; 649 } 650 subValue->retryPolicy = *retryPolicy; 651 subValue->updateRetryPolicy(); 652 } 653 654 EventServiceManager::getInstance().updateSubscriptionData(); 655 } 656 657 void doDelete(crow::Response& res, const crow::Request& req, 658 const std::vector<std::string>& params) override 659 { 660 auto asyncResp = std::make_shared<AsyncResp>(res); 661 662 if (params.size() != 1) 663 { 664 messages::internalError(asyncResp->res); 665 return; 666 } 667 668 if (!EventServiceManager::getInstance().isSubscriptionExist(params[0])) 669 { 670 res.result(boost::beast::http::status::not_found); 671 res.end(); 672 return; 673 } 674 EventServiceManager::getInstance().deleteSubscription(params[0]); 675 } 676 }; 677 678 } // namespace redfish 679