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 char* filters = req.urlParams.get("$filter"); 475 if (filters == nullptr) 476 { 477 subValue->eventFormatType = "Event"; 478 } 479 else 480 { 481 // Reading from query params. 482 bool status = readSSEQueryParams( 483 filters, subValue->eventFormatType, subValue->registryMsgIds, 484 subValue->registryPrefixes, subValue->metricReportDefinitions); 485 486 if (!status) 487 { 488 messages::invalidObject(res, filters); 489 return; 490 } 491 492 if (!subValue->eventFormatType.empty()) 493 { 494 if (std::find(supportedEvtFormatTypes.begin(), 495 supportedEvtFormatTypes.end(), 496 subValue->eventFormatType) == 497 supportedEvtFormatTypes.end()) 498 { 499 messages::propertyValueNotInList( 500 res, subValue->eventFormatType, "EventFormatType"); 501 return; 502 } 503 } 504 else 505 { 506 // If nothing specified, using default "Event" 507 subValue->eventFormatType.assign({"Event"}); 508 } 509 510 if (!subValue->registryPrefixes.empty()) 511 { 512 for (const std::string& it : subValue->registryPrefixes) 513 { 514 if (std::find(supportedRegPrefixes.begin(), 515 supportedRegPrefixes.end(), 516 it) == supportedRegPrefixes.end()) 517 { 518 messages::propertyValueNotInList(res, it, 519 "RegistryPrefixes"); 520 return; 521 } 522 } 523 } 524 } 525 526 std::string id = 527 EventServiceManager::getInstance().addSubscription(subValue, false); 528 if (id.empty()) 529 { 530 messages::internalError(res); 531 res.end(); 532 return; 533 } 534 } 535 }; 536 537 class EventDestination : public Node 538 { 539 public: 540 EventDestination(CrowApp& app) : 541 Node(app, "/redfish/v1/EventService/Subscriptions/<str>/", 542 std::string()) 543 { 544 entityPrivileges = { 545 {boost::beast::http::verb::get, {{"Login"}}}, 546 {boost::beast::http::verb::head, {{"Login"}}}, 547 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 548 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 549 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 550 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 551 } 552 553 private: 554 void doGet(crow::Response& res, const crow::Request& req, 555 const std::vector<std::string>& params) override 556 { 557 auto asyncResp = std::make_shared<AsyncResp>(res); 558 if (params.size() != 1) 559 { 560 messages::internalError(asyncResp->res); 561 return; 562 } 563 564 std::shared_ptr<Subscription> subValue = 565 EventServiceManager::getInstance().getSubscription(params[0]); 566 if (subValue == nullptr) 567 { 568 res.result(boost::beast::http::status::not_found); 569 res.end(); 570 return; 571 } 572 const std::string& id = params[0]; 573 574 res.jsonValue = { 575 {"@odata.type", "#EventDestination.v1_7_0.EventDestination"}, 576 {"Protocol", "Redfish"}}; 577 asyncResp->res.jsonValue["@odata.id"] = 578 "/redfish/v1/EventService/Subscriptions/" + id; 579 asyncResp->res.jsonValue["Id"] = id; 580 asyncResp->res.jsonValue["Name"] = "Event Destination " + id; 581 asyncResp->res.jsonValue["Destination"] = subValue->destinationUrl; 582 asyncResp->res.jsonValue["Context"] = subValue->customText; 583 asyncResp->res.jsonValue["SubscriptionType"] = 584 subValue->subscriptionType; 585 asyncResp->res.jsonValue["HttpHeaders"] = subValue->httpHeaders; 586 asyncResp->res.jsonValue["EventFormatType"] = subValue->eventFormatType; 587 asyncResp->res.jsonValue["RegistryPrefixes"] = 588 subValue->registryPrefixes; 589 asyncResp->res.jsonValue["ResourceTypes"] = subValue->resourceTypes; 590 591 asyncResp->res.jsonValue["MessageIds"] = subValue->registryMsgIds; 592 asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue->retryPolicy; 593 asyncResp->res.jsonValue["MetricReportDefinitions"] = 594 subValue->metricReportDefinitions; 595 } 596 597 void doPatch(crow::Response& res, const crow::Request& req, 598 const std::vector<std::string>& params) override 599 { 600 auto asyncResp = std::make_shared<AsyncResp>(res); 601 if (params.size() != 1) 602 { 603 messages::internalError(asyncResp->res); 604 return; 605 } 606 607 std::shared_ptr<Subscription> subValue = 608 EventServiceManager::getInstance().getSubscription(params[0]); 609 if (subValue == nullptr) 610 { 611 res.result(boost::beast::http::status::not_found); 612 res.end(); 613 return; 614 } 615 616 std::optional<std::string> context; 617 std::optional<std::string> retryPolicy; 618 std::optional<std::vector<nlohmann::json>> headers; 619 620 if (!json_util::readJson(req, res, "Context", context, 621 "DeliveryRetryPolicy", retryPolicy, 622 "HttpHeaders", headers)) 623 { 624 return; 625 } 626 627 if (context) 628 { 629 subValue->customText = *context; 630 } 631 632 if (headers) 633 { 634 subValue->httpHeaders = *headers; 635 } 636 637 if (retryPolicy) 638 { 639 if (std::find(supportedRetryPolicies.begin(), 640 supportedRetryPolicies.end(), 641 *retryPolicy) == supportedRetryPolicies.end()) 642 { 643 messages::propertyValueNotInList(asyncResp->res, *retryPolicy, 644 "DeliveryRetryPolicy"); 645 return; 646 } 647 subValue->retryPolicy = *retryPolicy; 648 subValue->updateRetryPolicy(); 649 } 650 651 EventServiceManager::getInstance().updateSubscriptionData(); 652 } 653 654 void doDelete(crow::Response& res, const crow::Request& req, 655 const std::vector<std::string>& params) override 656 { 657 auto asyncResp = std::make_shared<AsyncResp>(res); 658 659 if (params.size() != 1) 660 { 661 messages::internalError(asyncResp->res); 662 return; 663 } 664 665 if (!EventServiceManager::getInstance().isSubscriptionExist(params[0])) 666 { 667 res.result(boost::beast::http::status::not_found); 668 res.end(); 669 return; 670 } 671 EventServiceManager::getInstance().deleteSubscription(params[0]); 672 } 673 }; 674 675 } // namespace redfish 676