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