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 if (regPrefixes && msgIds) 254 { 255 if (regPrefixes->size() && msgIds->size()) 256 { 257 messages::mutualExclusiveProperties( 258 asyncResp->res, "RegistryPrefixes", "MessageIds"); 259 return; 260 } 261 } 262 263 // Validate the URL using regex expression 264 // Format: <protocol>://<host>:<port>/<uri> 265 // protocol: http/https 266 // host: Exclude ' ', ':', '#', '?' 267 // port: Empty or numeric value with ':' separator. 268 // uri: Start with '/' and Exclude '#', ' ' 269 // Can include query params(ex: '/event?test=1') 270 // TODO: Need to validate hostname extensively(as per rfc) 271 const std::regex urlRegex( 272 "(http|https)://([^/\\x20\\x3f\\x23\\x3a]+):?([0-9]*)(/" 273 "([^\\x20\\x23\\x3f]*\\x3f?([^\\x20\\x23\\x3f])*)?)"); 274 std::cmatch match; 275 if (!std::regex_match(destUrl.c_str(), match, urlRegex)) 276 { 277 messages::propertyValueFormatError(asyncResp->res, destUrl, 278 "Destination"); 279 return; 280 } 281 282 std::string uriProto = std::string(match[1].first, match[1].second); 283 if (uriProto == "http") 284 { 285 #ifndef BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING 286 messages::propertyValueFormatError(asyncResp->res, destUrl, 287 "Destination"); 288 return; 289 #endif 290 } 291 292 std::string host = std::string(match[2].first, match[2].second); 293 std::string port = std::string(match[3].first, match[3].second); 294 std::string path = std::string(match[4].first, match[4].second); 295 if (port.empty()) 296 { 297 if (uriProto == "http") 298 { 299 port = "80"; 300 } 301 else 302 { 303 port = "443"; 304 } 305 } 306 if (path.empty()) 307 { 308 path = "/"; 309 } 310 311 std::shared_ptr<Subscription> subValue = 312 std::make_shared<Subscription>(host, port, path, uriProto); 313 314 subValue->destinationUrl = destUrl; 315 316 if (subscriptionType) 317 { 318 if (*subscriptionType != "RedfishEvent") 319 { 320 messages::propertyValueNotInList( 321 asyncResp->res, *subscriptionType, "SubscriptionType"); 322 return; 323 } 324 subValue->subscriptionType = *subscriptionType; 325 } 326 else 327 { 328 subValue->subscriptionType = "RedfishEvent"; // Default 329 } 330 331 if (protocol != "Redfish") 332 { 333 messages::propertyValueNotInList(asyncResp->res, protocol, 334 "Protocol"); 335 return; 336 } 337 subValue->protocol = protocol; 338 339 if (eventFormatType2) 340 { 341 if (std::find(supportedEvtFormatTypes.begin(), 342 supportedEvtFormatTypes.end(), 343 *eventFormatType2) == supportedEvtFormatTypes.end()) 344 { 345 messages::propertyValueNotInList( 346 asyncResp->res, *eventFormatType2, "EventFormatType"); 347 return; 348 } 349 subValue->eventFormatType = *eventFormatType2; 350 } 351 else 352 { 353 // If not specified, use default "Event" 354 subValue->eventFormatType = "Event"; 355 } 356 357 if (context) 358 { 359 subValue->customText = *context; 360 } 361 362 if (headers) 363 { 364 subValue->httpHeaders = *headers; 365 } 366 367 if (regPrefixes) 368 { 369 for (const std::string& it : *regPrefixes) 370 { 371 if (std::find(supportedRegPrefixes.begin(), 372 supportedRegPrefixes.end(), 373 it) == supportedRegPrefixes.end()) 374 { 375 messages::propertyValueNotInList(asyncResp->res, it, 376 "RegistryPrefixes"); 377 return; 378 } 379 } 380 subValue->registryPrefixes = *regPrefixes; 381 } 382 383 if (resTypes) 384 { 385 for (const std::string& it : *resTypes) 386 { 387 if (std::find(supportedResourceTypes.begin(), 388 supportedResourceTypes.end(), 389 it) == supportedResourceTypes.end()) 390 { 391 messages::propertyValueNotInList(asyncResp->res, it, 392 "ResourceTypes"); 393 return; 394 } 395 } 396 subValue->resourceTypes = *resTypes; 397 } 398 399 if (msgIds) 400 { 401 // Do we need to loop-up MessageRegistry and validate 402 // data for authenticity??? Not mandate, i believe. 403 subValue->registryMsgIds = *msgIds; 404 } 405 406 if (retryPolicy) 407 { 408 if (std::find(supportedRetryPolicies.begin(), 409 supportedRetryPolicies.end(), 410 *retryPolicy) == supportedRetryPolicies.end()) 411 { 412 messages::propertyValueNotInList(asyncResp->res, *retryPolicy, 413 "DeliveryRetryPolicy"); 414 return; 415 } 416 subValue->retryPolicy = *retryPolicy; 417 } 418 else 419 { 420 // Default "TerminateAfterRetries" 421 subValue->retryPolicy = "TerminateAfterRetries"; 422 } 423 424 if (mrdJsonArray) 425 { 426 for (nlohmann::json& mrdObj : *mrdJsonArray) 427 { 428 std::string mrdUri; 429 if (json_util::getValueFromJsonObject(mrdObj, "@odata.id", 430 mrdUri)) 431 { 432 subValue->metricReportDefinitions.emplace_back(mrdUri); 433 } 434 else 435 { 436 messages::propertyValueFormatError( 437 asyncResp->res, mrdObj.dump(), 438 "MetricReportDefinitions"); 439 return; 440 } 441 } 442 } 443 444 std::string id = 445 EventServiceManager::getInstance().addSubscription(subValue); 446 if (id.empty()) 447 { 448 messages::internalError(asyncResp->res); 449 return; 450 } 451 452 messages::created(asyncResp->res); 453 asyncResp->res.addHeader( 454 "Location", "/redfish/v1/EventService/Subscriptions/" + id); 455 } 456 }; 457 458 class EventDestination : public Node 459 { 460 public: 461 EventDestination(App& app) : 462 Node(app, "/redfish/v1/EventService/Subscriptions/<str>/", 463 std::string()) 464 { 465 entityPrivileges = { 466 {boost::beast::http::verb::get, {{"Login"}}}, 467 {boost::beast::http::verb::head, {{"Login"}}}, 468 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 469 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 470 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 471 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 472 } 473 474 private: 475 void doGet(crow::Response& res, const crow::Request&, 476 const std::vector<std::string>& params) override 477 { 478 auto asyncResp = std::make_shared<AsyncResp>(res); 479 if (params.size() != 1) 480 { 481 messages::internalError(asyncResp->res); 482 return; 483 } 484 485 std::shared_ptr<Subscription> subValue = 486 EventServiceManager::getInstance().getSubscription(params[0]); 487 if (subValue == nullptr) 488 { 489 res.result(boost::beast::http::status::not_found); 490 res.end(); 491 return; 492 } 493 const std::string& id = params[0]; 494 495 res.jsonValue = { 496 {"@odata.type", "#EventDestination.v1_7_0.EventDestination"}, 497 {"Protocol", "Redfish"}}; 498 asyncResp->res.jsonValue["@odata.id"] = 499 "/redfish/v1/EventService/Subscriptions/" + id; 500 asyncResp->res.jsonValue["Id"] = id; 501 asyncResp->res.jsonValue["Name"] = "Event Destination " + id; 502 asyncResp->res.jsonValue["Destination"] = subValue->destinationUrl; 503 asyncResp->res.jsonValue["Context"] = subValue->customText; 504 asyncResp->res.jsonValue["SubscriptionType"] = 505 subValue->subscriptionType; 506 asyncResp->res.jsonValue["HttpHeaders"] = subValue->httpHeaders; 507 asyncResp->res.jsonValue["EventFormatType"] = subValue->eventFormatType; 508 asyncResp->res.jsonValue["RegistryPrefixes"] = 509 subValue->registryPrefixes; 510 asyncResp->res.jsonValue["ResourceTypes"] = subValue->resourceTypes; 511 512 asyncResp->res.jsonValue["MessageIds"] = subValue->registryMsgIds; 513 asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue->retryPolicy; 514 515 std::vector<nlohmann::json> mrdJsonArray; 516 for (const auto& mdrUri : subValue->metricReportDefinitions) 517 { 518 mrdJsonArray.push_back({{"@odata.id", mdrUri}}); 519 } 520 asyncResp->res.jsonValue["MetricReportDefinitions"] = mrdJsonArray; 521 } 522 523 void doPatch(crow::Response& res, const crow::Request& req, 524 const std::vector<std::string>& params) override 525 { 526 auto asyncResp = std::make_shared<AsyncResp>(res); 527 if (params.size() != 1) 528 { 529 messages::internalError(asyncResp->res); 530 return; 531 } 532 533 std::shared_ptr<Subscription> subValue = 534 EventServiceManager::getInstance().getSubscription(params[0]); 535 if (subValue == nullptr) 536 { 537 res.result(boost::beast::http::status::not_found); 538 res.end(); 539 return; 540 } 541 542 std::optional<std::string> context; 543 std::optional<std::string> retryPolicy; 544 std::optional<std::vector<nlohmann::json>> headers; 545 546 if (!json_util::readJson(req, res, "Context", context, 547 "DeliveryRetryPolicy", retryPolicy, 548 "HttpHeaders", headers)) 549 { 550 return; 551 } 552 553 if (context) 554 { 555 subValue->customText = *context; 556 } 557 558 if (headers) 559 { 560 subValue->httpHeaders = *headers; 561 } 562 563 if (retryPolicy) 564 { 565 if (std::find(supportedRetryPolicies.begin(), 566 supportedRetryPolicies.end(), 567 *retryPolicy) == supportedRetryPolicies.end()) 568 { 569 messages::propertyValueNotInList(asyncResp->res, *retryPolicy, 570 "DeliveryRetryPolicy"); 571 return; 572 } 573 subValue->retryPolicy = *retryPolicy; 574 subValue->updateRetryPolicy(); 575 } 576 577 EventServiceManager::getInstance().updateSubscriptionData(); 578 } 579 580 void doDelete(crow::Response& res, const crow::Request&, 581 const std::vector<std::string>& params) override 582 { 583 auto asyncResp = std::make_shared<AsyncResp>(res); 584 585 if (params.size() != 1) 586 { 587 messages::internalError(asyncResp->res); 588 return; 589 } 590 591 if (!EventServiceManager::getInstance().isSubscriptionExist(params[0])) 592 { 593 res.result(boost::beast::http::status::not_found); 594 res.end(); 595 return; 596 } 597 EventServiceManager::getInstance().deleteSubscription(params[0]); 598 } 599 }; 600 601 } // namespace redfish 602