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