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