1 #pragma once 2 #include "bmcweb_config.h" 3 4 #include "app.hpp" 5 #include "async_resp.hpp" 6 #include "error_messages.hpp" 7 #include "http_request.hpp" 8 #include "http_response.hpp" 9 #include "logging.hpp" 10 11 #include <sys/types.h> 12 13 #include <boost/beast/http/message.hpp> // IWYU pragma: keep 14 #include <boost/beast/http/status.hpp> 15 #include <boost/beast/http/verb.hpp> 16 #include <boost/url/params_view.hpp> 17 #include <boost/url/string.hpp> 18 #include <nlohmann/json.hpp> 19 20 #include <algorithm> 21 #include <charconv> 22 #include <cstdint> 23 #include <functional> 24 #include <limits> 25 #include <map> 26 #include <memory> 27 #include <optional> 28 #include <string> 29 #include <string_view> 30 #include <system_error> 31 #include <utility> 32 #include <vector> 33 34 // IWYU pragma: no_include <boost/url/impl/params_view.hpp> 35 // IWYU pragma: no_include <boost/beast/http/impl/message.hpp> 36 // IWYU pragma: no_include <boost/intrusive/detail/list_iterator.hpp> 37 // IWYU pragma: no_include <stdint.h> 38 39 namespace redfish 40 { 41 namespace query_param 42 { 43 inline constexpr size_t maxEntriesPerPage = 1000; 44 45 enum class ExpandType : uint8_t 46 { 47 None, 48 Links, 49 NotLinks, 50 Both, 51 }; 52 53 // The struct stores the parsed query parameters of the default Redfish route. 54 struct Query 55 { 56 // Only 57 bool isOnly = false; 58 // Expand 59 uint8_t expandLevel = 0; 60 ExpandType expandType = ExpandType::None; 61 62 // Skip 63 std::optional<size_t> skip = std::nullopt; 64 65 // Top 66 std::optional<size_t> top = std::nullopt; 67 }; 68 69 // The struct defines how resource handlers in redfish-core/lib/ can handle 70 // query parameters themselves, so that the default Redfish route will delegate 71 // the processing. 72 struct QueryCapabilities 73 { 74 bool canDelegateOnly = false; 75 bool canDelegateTop = false; 76 bool canDelegateSkip = false; 77 uint8_t canDelegateExpandLevel = 0; 78 }; 79 80 // Delegates query parameters according to the given |queryCapabilities| 81 // This function doesn't check query parameter conflicts since the parse 82 // function will take care of it. 83 // Returns a delegated query object which can be used by individual resource 84 // handlers so that handlers don't need to query again. 85 inline Query delegate(const QueryCapabilities& queryCapabilities, Query& query) 86 { 87 Query delegated; 88 // delegate only 89 if (query.isOnly && queryCapabilities.canDelegateOnly) 90 { 91 delegated.isOnly = true; 92 query.isOnly = false; 93 } 94 // delegate expand as much as we can 95 if (query.expandType != ExpandType::None) 96 { 97 delegated.expandType = query.expandType; 98 if (query.expandLevel <= queryCapabilities.canDelegateExpandLevel) 99 { 100 query.expandType = ExpandType::None; 101 delegated.expandLevel = query.expandLevel; 102 query.expandLevel = 0; 103 } 104 else 105 { 106 query.expandLevel -= queryCapabilities.canDelegateExpandLevel; 107 delegated.expandLevel = queryCapabilities.canDelegateExpandLevel; 108 } 109 } 110 111 // delegate top 112 if (query.top && queryCapabilities.canDelegateTop) 113 { 114 delegated.top = query.top; 115 query.top = std::nullopt; 116 } 117 118 // delegate skip 119 if (query.skip && queryCapabilities.canDelegateSkip) 120 { 121 delegated.skip = query.skip; 122 query.skip = 0; 123 } 124 return delegated; 125 } 126 127 inline bool getExpandType(std::string_view value, Query& query) 128 { 129 if (value.empty()) 130 { 131 return false; 132 } 133 switch (value[0]) 134 { 135 case '*': 136 query.expandType = ExpandType::Both; 137 break; 138 case '.': 139 query.expandType = ExpandType::NotLinks; 140 break; 141 case '~': 142 query.expandType = ExpandType::Links; 143 break; 144 default: 145 return false; 146 147 break; 148 } 149 value.remove_prefix(1); 150 if (value.empty()) 151 { 152 query.expandLevel = 1; 153 return true; 154 } 155 constexpr std::string_view levels = "($levels="; 156 if (!value.starts_with(levels)) 157 { 158 return false; 159 } 160 value.remove_prefix(levels.size()); 161 162 auto it = std::from_chars(value.data(), value.data() + value.size(), 163 query.expandLevel); 164 if (it.ec != std::errc()) 165 { 166 return false; 167 } 168 value.remove_prefix(static_cast<size_t>(it.ptr - value.data())); 169 return value == ")"; 170 } 171 172 enum class QueryError 173 { 174 Ok, 175 OutOfRange, 176 ValueFormat, 177 }; 178 179 inline QueryError getNumericParam(std::string_view value, size_t& param) 180 { 181 std::from_chars_result r = 182 std::from_chars(value.data(), value.data() + value.size(), param); 183 184 // If the number wasn't representable in the type, it's out of range 185 if (r.ec == std::errc::result_out_of_range) 186 { 187 return QueryError::OutOfRange; 188 } 189 // All other errors are value format 190 if (r.ec != std::errc()) 191 { 192 return QueryError::ValueFormat; 193 } 194 return QueryError::Ok; 195 } 196 197 inline QueryError getSkipParam(std::string_view value, Query& query) 198 { 199 return getNumericParam(value, query.skip.emplace()); 200 } 201 202 inline QueryError getTopParam(std::string_view value, Query& query) 203 { 204 QueryError ret = getNumericParam(value, query.top.emplace()); 205 if (ret != QueryError::Ok) 206 { 207 return ret; 208 } 209 210 // Range check for sanity. 211 if (query.top > maxEntriesPerPage) 212 { 213 return QueryError::OutOfRange; 214 } 215 216 return QueryError::Ok; 217 } 218 219 inline std::optional<Query> 220 parseParameters(const boost::urls::params_view& urlParams, 221 crow::Response& res) 222 { 223 Query ret; 224 for (const boost::urls::params_view::value_type& it : urlParams) 225 { 226 std::string_view key(it.key.data(), it.key.size()); 227 std::string_view value(it.value.data(), it.value.size()); 228 if (key == "only") 229 { 230 if (!it.value.empty()) 231 { 232 messages::queryParameterValueFormatError(res, value, key); 233 return std::nullopt; 234 } 235 ret.isOnly = true; 236 } 237 else if (key == "$expand" && bmcwebInsecureEnableQueryParams) 238 { 239 if (!getExpandType(value, ret)) 240 { 241 messages::queryParameterValueFormatError(res, value, key); 242 return std::nullopt; 243 } 244 } 245 else if (key == "$top") 246 { 247 QueryError topRet = getTopParam(value, ret); 248 if (topRet == QueryError::ValueFormat) 249 { 250 messages::queryParameterValueFormatError(res, value, key); 251 return std::nullopt; 252 } 253 if (topRet == QueryError::OutOfRange) 254 { 255 messages::queryParameterOutOfRange( 256 res, value, "$top", 257 "0-" + std::to_string(maxEntriesPerPage)); 258 return std::nullopt; 259 } 260 } 261 else if (key == "$skip") 262 { 263 QueryError topRet = getSkipParam(value, ret); 264 if (topRet == QueryError::ValueFormat) 265 { 266 messages::queryParameterValueFormatError(res, value, key); 267 return std::nullopt; 268 } 269 if (topRet == QueryError::OutOfRange) 270 { 271 messages::queryParameterOutOfRange( 272 res, value, key, 273 "0-" + std::to_string(std::numeric_limits<size_t>::max())); 274 return std::nullopt; 275 } 276 } 277 else 278 { 279 // Intentionally ignore other errors Redfish spec, 7.3.1 280 if (key.starts_with("$")) 281 { 282 // Services shall return... The HTTP 501 Not Implemented 283 // status code for any unsupported query parameters that 284 // start with $ . 285 messages::queryParameterValueFormatError(res, value, key); 286 res.result(boost::beast::http::status::not_implemented); 287 return std::nullopt; 288 } 289 // "Shall ignore unknown or unsupported query parameters that do 290 // not begin with $ ." 291 } 292 } 293 294 return ret; 295 } 296 297 inline bool processOnly(crow::App& app, crow::Response& res, 298 std::function<void(crow::Response&)>& completionHandler) 299 { 300 BMCWEB_LOG_DEBUG << "Processing only query param"; 301 auto itMembers = res.jsonValue.find("Members"); 302 if (itMembers == res.jsonValue.end()) 303 { 304 messages::queryNotSupportedOnResource(res); 305 completionHandler(res); 306 return false; 307 } 308 auto itMemBegin = itMembers->begin(); 309 if (itMemBegin == itMembers->end() || itMembers->size() != 1) 310 { 311 BMCWEB_LOG_DEBUG << "Members contains " << itMembers->size() 312 << " element, returning full collection."; 313 completionHandler(res); 314 return false; 315 } 316 317 auto itUrl = itMemBegin->find("@odata.id"); 318 if (itUrl == itMemBegin->end()) 319 { 320 BMCWEB_LOG_DEBUG << "No found odata.id"; 321 messages::internalError(res); 322 completionHandler(res); 323 return false; 324 } 325 const std::string* url = itUrl->get_ptr<const std::string*>(); 326 if (url == nullptr) 327 { 328 BMCWEB_LOG_DEBUG << "@odata.id wasn't a string????"; 329 messages::internalError(res); 330 completionHandler(res); 331 return false; 332 } 333 // TODO(Ed) copy request headers? 334 // newReq.session = req.session; 335 std::error_code ec; 336 crow::Request newReq({boost::beast::http::verb::get, *url, 11}, ec); 337 if (ec) 338 { 339 messages::internalError(res); 340 completionHandler(res); 341 return false; 342 } 343 344 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(); 345 BMCWEB_LOG_DEBUG << "setting completion handler on " << &asyncResp->res; 346 asyncResp->res.setCompleteRequestHandler(std::move(completionHandler)); 347 asyncResp->res.setIsAliveHelper(res.releaseIsAliveHelper()); 348 app.handle(newReq, asyncResp); 349 return true; 350 } 351 352 struct ExpandNode 353 { 354 nlohmann::json::json_pointer location; 355 std::string uri; 356 357 inline bool operator==(const ExpandNode& other) const 358 { 359 return location == other.location && uri == other.uri; 360 } 361 }; 362 363 // Walks a json object looking for Redfish NavigationReference entries that 364 // might need resolved. It recursively walks the jsonResponse object, looking 365 // for links at every level, and returns a list (out) of locations within the 366 // tree that need to be expanded. The current json pointer location p is passed 367 // in to reference the current node that's being expanded, so it can be combined 368 // with the keys from the jsonResponse object 369 inline void findNavigationReferencesRecursive( 370 ExpandType eType, nlohmann::json& jsonResponse, 371 const nlohmann::json::json_pointer& p, bool inLinks, 372 std::vector<ExpandNode>& out) 373 { 374 // If no expand is needed, return early 375 if (eType == ExpandType::None) 376 { 377 return; 378 } 379 nlohmann::json::array_t* array = 380 jsonResponse.get_ptr<nlohmann::json::array_t*>(); 381 if (array != nullptr) 382 { 383 size_t index = 0; 384 // For arrays, walk every element in the array 385 for (auto& element : *array) 386 { 387 nlohmann::json::json_pointer newPtr = p / index; 388 BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr.to_string(); 389 findNavigationReferencesRecursive(eType, element, newPtr, inLinks, 390 out); 391 index++; 392 } 393 } 394 nlohmann::json::object_t* obj = 395 jsonResponse.get_ptr<nlohmann::json::object_t*>(); 396 if (obj == nullptr) 397 { 398 return; 399 } 400 // Navigation References only ever have a single element 401 if (obj->size() == 1) 402 { 403 if (obj->begin()->first == "@odata.id") 404 { 405 const std::string* uri = 406 obj->begin()->second.get_ptr<const std::string*>(); 407 if (uri != nullptr) 408 { 409 BMCWEB_LOG_DEBUG << "Found element at " << p.to_string(); 410 out.push_back({p, *uri}); 411 } 412 } 413 } 414 // Loop the object and look for links 415 for (auto& element : *obj) 416 { 417 bool localInLinks = inLinks; 418 if (!localInLinks) 419 { 420 // Check if this is a links node 421 localInLinks = element.first == "Links"; 422 } 423 // Only traverse the parts of the tree the user asked for 424 // Per section 7.3 of the redfish specification 425 if (localInLinks && eType == ExpandType::NotLinks) 426 { 427 continue; 428 } 429 if (!localInLinks && eType == ExpandType::Links) 430 { 431 continue; 432 } 433 nlohmann::json::json_pointer newPtr = p / element.first; 434 BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr; 435 436 findNavigationReferencesRecursive(eType, element.second, newPtr, 437 localInLinks, out); 438 } 439 } 440 441 inline std::vector<ExpandNode> 442 findNavigationReferences(ExpandType eType, nlohmann::json& jsonResponse) 443 { 444 std::vector<ExpandNode> ret; 445 const nlohmann::json::json_pointer root = nlohmann::json::json_pointer(""); 446 findNavigationReferencesRecursive(eType, jsonResponse, root, false, ret); 447 return ret; 448 } 449 450 // Formats a query parameter string for the sub-query. 451 // Returns std::nullopt on failures. 452 // This function shall handle $select when it is added. 453 // There is no need to handle parameters that's not campatible with $expand, 454 // e.g., $only, since this function will only be called in side $expand handlers 455 inline std::optional<std::string> formatQueryForExpand(const Query& query) 456 { 457 // query.expandLevel<=1: no need to do subqueries 458 if (query.expandLevel <= 1) 459 { 460 return ""; 461 } 462 std::string str = "?$expand="; 463 bool queryTypeExpected = false; 464 switch (query.expandType) 465 { 466 case ExpandType::None: 467 return ""; 468 case ExpandType::Links: 469 queryTypeExpected = true; 470 str += '~'; 471 break; 472 case ExpandType::NotLinks: 473 queryTypeExpected = true; 474 str += '.'; 475 break; 476 case ExpandType::Both: 477 queryTypeExpected = true; 478 str += '*'; 479 break; 480 } 481 if (!queryTypeExpected) 482 { 483 return std::nullopt; 484 } 485 str += "($levels="; 486 str += std::to_string(query.expandLevel - 1); 487 str += ')'; 488 return str; 489 } 490 491 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp> 492 { 493 public: 494 // This object takes a single asyncResp object as the "final" one, then 495 // allows callers to attach sub-responses within the json tree that need 496 // to be executed and filled into their appropriate locations. This 497 // class manages the final "merge" of the json resources. 498 MultiAsyncResp(crow::App& appIn, 499 std::shared_ptr<bmcweb::AsyncResp> finalResIn) : 500 app(appIn), 501 finalRes(std::move(finalResIn)) 502 {} 503 504 void addAwaitingResponse( 505 const std::shared_ptr<bmcweb::AsyncResp>& res, 506 const nlohmann::json::json_pointer& finalExpandLocation) 507 { 508 res->res.setCompleteRequestHandler(std::bind_front( 509 placeResultStatic, shared_from_this(), finalExpandLocation)); 510 } 511 512 void placeResult(const nlohmann::json::json_pointer& locationToPlace, 513 crow::Response& res) 514 { 515 nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace]; 516 finalObj = std::move(res.jsonValue); 517 } 518 519 // Handles the very first level of Expand, and starts a chain of sub-queries 520 // for deeper levels. 521 void startQuery(const Query& query) 522 { 523 std::vector<ExpandNode> nodes = 524 findNavigationReferences(query.expandType, finalRes->res.jsonValue); 525 BMCWEB_LOG_DEBUG << nodes.size() << " nodes to traverse"; 526 const std::optional<std::string> queryStr = formatQueryForExpand(query); 527 if (!queryStr) 528 { 529 messages::internalError(finalRes->res); 530 return; 531 } 532 for (const ExpandNode& node : nodes) 533 { 534 const std::string subQuery = node.uri + *queryStr; 535 BMCWEB_LOG_DEBUG << "URL of subquery: " << subQuery; 536 std::error_code ec; 537 crow::Request newReq({boost::beast::http::verb::get, subQuery, 11}, 538 ec); 539 if (ec) 540 { 541 messages::internalError(finalRes->res); 542 return; 543 } 544 545 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(); 546 BMCWEB_LOG_DEBUG << "setting completion handler on " 547 << &asyncResp->res; 548 549 addAwaitingResponse(asyncResp, node.location); 550 app.handle(newReq, asyncResp); 551 } 552 } 553 554 private: 555 static void 556 placeResultStatic(const std::shared_ptr<MultiAsyncResp>& multi, 557 const nlohmann::json::json_pointer& locationToPlace, 558 crow::Response& res) 559 { 560 multi->placeResult(locationToPlace, res); 561 } 562 563 crow::App& app; 564 std::shared_ptr<bmcweb::AsyncResp> finalRes; 565 }; 566 567 inline void processTopAndSkip(const Query& query, crow::Response& res) 568 { 569 if (!query.skip && !query.top) 570 { 571 // No work to do. 572 return; 573 } 574 nlohmann::json::object_t* obj = 575 res.jsonValue.get_ptr<nlohmann::json::object_t*>(); 576 if (obj == nullptr) 577 { 578 // Shouldn't be possible. All responses should be objects. 579 messages::internalError(res); 580 return; 581 } 582 583 BMCWEB_LOG_DEBUG << "Handling top/skip"; 584 nlohmann::json::object_t::iterator members = obj->find("Members"); 585 if (members == obj->end()) 586 { 587 // From the Redfish specification 7.3.1 588 // ... the HTTP 400 Bad Request status code with the 589 // QueryNotSupportedOnResource message from the Base Message Registry 590 // for any supported query parameters that apply only to resource 591 // collections but are used on singular resources. 592 messages::queryNotSupportedOnResource(res); 593 return; 594 } 595 596 nlohmann::json::array_t* arr = 597 members->second.get_ptr<nlohmann::json::array_t*>(); 598 if (arr == nullptr) 599 { 600 messages::internalError(res); 601 return; 602 } 603 604 if (query.skip) 605 { 606 // Per section 7.3.1 of the Redfish specification, $skip is run before 607 // $top Can only skip as many values as we have 608 size_t skip = std::min(arr->size(), *query.skip); 609 arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip)); 610 } 611 if (query.top) 612 { 613 size_t top = std::min(arr->size(), *query.top); 614 arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end()); 615 } 616 } 617 618 inline void 619 processAllParams(crow::App& app, const Query& query, 620 std::function<void(crow::Response&)>& completionHandler, 621 crow::Response& intermediateResponse) 622 { 623 if (!completionHandler) 624 { 625 BMCWEB_LOG_DEBUG << "Function was invalid?"; 626 return; 627 } 628 629 BMCWEB_LOG_DEBUG << "Processing query params"; 630 // If the request failed, there's no reason to even try to run query 631 // params. 632 if (intermediateResponse.resultInt() < 200 || 633 intermediateResponse.resultInt() >= 400) 634 { 635 completionHandler(intermediateResponse); 636 return; 637 } 638 if (query.isOnly) 639 { 640 processOnly(app, intermediateResponse, completionHandler); 641 return; 642 } 643 644 if (query.top || query.skip) 645 { 646 processTopAndSkip(query, intermediateResponse); 647 } 648 649 if (query.expandType != ExpandType::None) 650 { 651 BMCWEB_LOG_DEBUG << "Executing expand query"; 652 // TODO(ed) this is a copy of the response object. Admittedly, 653 // we're inherently doing something inefficient, but we shouldn't 654 // have to do a full copy 655 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(); 656 asyncResp->res.setCompleteRequestHandler(std::move(completionHandler)); 657 asyncResp->res.jsonValue = std::move(intermediateResponse.jsonValue); 658 auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp); 659 660 multi->startQuery(query); 661 return; 662 } 663 completionHandler(intermediateResponse); 664 } 665 666 } // namespace query_param 667 } // namespace redfish 668