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 // This function shall handle $select when it is added. 427 // There is no need to handle parameters that's not campatible with $expand, 428 // e.g., $only, since this function will only be called in side $expand handlers 429 inline std::string formatQueryForExpand(const Query& query) 430 { 431 // query.expandLevel<=1: no need to do subqueries 432 if (query.expandLevel <= 1) 433 { 434 return {}; 435 } 436 std::string str = "?$expand="; 437 switch (query.expandType) 438 { 439 case ExpandType::None: 440 return {}; 441 case ExpandType::Links: 442 str += '~'; 443 break; 444 case ExpandType::NotLinks: 445 str += '.'; 446 break; 447 case ExpandType::Both: 448 str += '*'; 449 break; 450 default: 451 return {}; 452 } 453 str += "($levels="; 454 str += std::to_string(query.expandLevel - 1); 455 str += ')'; 456 return str; 457 } 458 459 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp> 460 { 461 public: 462 // This object takes a single asyncResp object as the "final" one, then 463 // allows callers to attach sub-responses within the json tree that need 464 // to be executed and filled into their appropriate locations. This 465 // class manages the final "merge" of the json resources. 466 MultiAsyncResp(crow::App& app, 467 std::shared_ptr<bmcweb::AsyncResp> finalResIn) : 468 app(app), 469 finalRes(std::move(finalResIn)) 470 {} 471 472 void addAwaitingResponse( 473 std::shared_ptr<bmcweb::AsyncResp>& res, 474 const nlohmann::json::json_pointer& finalExpandLocation) 475 { 476 res->res.setCompleteRequestHandler(std::bind_front( 477 placeResultStatic, shared_from_this(), finalExpandLocation)); 478 } 479 480 void placeResult(const nlohmann::json::json_pointer& locationToPlace, 481 crow::Response& res) 482 { 483 nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace]; 484 finalObj = std::move(res.jsonValue); 485 } 486 487 // Handles the very first level of Expand, and starts a chain of sub-queries 488 // for deeper levels. 489 void startQuery(const Query& query) 490 { 491 std::vector<ExpandNode> nodes = 492 findNavigationReferences(query.expandType, finalRes->res.jsonValue); 493 BMCWEB_LOG_DEBUG << nodes.size() << " nodes to traverse"; 494 const std::string queryStr = formatQueryForExpand(query); 495 for (const ExpandNode& node : nodes) 496 { 497 const std::string subQuery = node.uri + queryStr; 498 BMCWEB_LOG_DEBUG << "URL of subquery: " << subQuery; 499 std::error_code ec; 500 crow::Request newReq({boost::beast::http::verb::get, subQuery, 11}, 501 ec); 502 if (ec) 503 { 504 messages::internalError(finalRes->res); 505 return; 506 } 507 508 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(); 509 BMCWEB_LOG_DEBUG << "setting completion handler on " 510 << &asyncResp->res; 511 512 addAwaitingResponse(asyncResp, node.location); 513 app.handle(newReq, asyncResp); 514 } 515 } 516 517 private: 518 static void 519 placeResultStatic(const std::shared_ptr<MultiAsyncResp>& multi, 520 const nlohmann::json::json_pointer& locationToPlace, 521 crow::Response& res) 522 { 523 multi->placeResult(locationToPlace, res); 524 } 525 526 crow::App& app; 527 std::shared_ptr<bmcweb::AsyncResp> finalRes; 528 }; 529 530 inline void processTopAndSkip(const Query& query, crow::Response& res) 531 { 532 nlohmann::json::object_t* obj = 533 res.jsonValue.get_ptr<nlohmann::json::object_t*>(); 534 if (obj == nullptr) 535 { 536 // Shouldn't be possible. All responses should be objects. 537 messages::internalError(res); 538 return; 539 } 540 541 BMCWEB_LOG_DEBUG << "Handling top/skip"; 542 nlohmann::json::object_t::iterator members = obj->find("Members"); 543 if (members == obj->end()) 544 { 545 // From the Redfish specification 7.3.1 546 // ... the HTTP 400 Bad Request status code with the 547 // QueryNotSupportedOnResource message from the Base Message Registry 548 // for any supported query parameters that apply only to resource 549 // collections but are used on singular resources. 550 messages::queryNotSupportedOnResource(res); 551 return; 552 } 553 554 nlohmann::json::array_t* arr = 555 members->second.get_ptr<nlohmann::json::array_t*>(); 556 if (arr == nullptr) 557 { 558 messages::internalError(res); 559 return; 560 } 561 562 // Per section 7.3.1 of the Redfish specification, $skip is run before $top 563 // Can only skip as many values as we have 564 size_t skip = std::min(arr->size(), query.skip); 565 arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip)); 566 567 size_t top = std::min(arr->size(), query.top); 568 arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end()); 569 } 570 571 inline void 572 processAllParams(crow::App& app, const Query& query, 573 std::function<void(crow::Response&)>& completionHandler, 574 crow::Response& intermediateResponse) 575 { 576 if (!completionHandler) 577 { 578 BMCWEB_LOG_DEBUG << "Function was invalid?"; 579 return; 580 } 581 582 BMCWEB_LOG_DEBUG << "Processing query params"; 583 // If the request failed, there's no reason to even try to run query 584 // params. 585 if (intermediateResponse.resultInt() < 200 || 586 intermediateResponse.resultInt() >= 400) 587 { 588 completionHandler(intermediateResponse); 589 return; 590 } 591 if (query.isOnly) 592 { 593 processOnly(app, intermediateResponse, completionHandler); 594 return; 595 } 596 597 if (query.top != std::numeric_limits<size_t>::max() || query.skip != 0) 598 { 599 processTopAndSkip(query, intermediateResponse); 600 } 601 602 if (query.expandType != ExpandType::None) 603 { 604 BMCWEB_LOG_DEBUG << "Executing expand query"; 605 // TODO(ed) this is a copy of the response object. Admittedly, 606 // we're inherently doing something inefficient, but we shouldn't 607 // have to do a full copy 608 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(); 609 asyncResp->res.setCompleteRequestHandler(std::move(completionHandler)); 610 asyncResp->res.jsonValue = std::move(intermediateResponse.jsonValue); 611 auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp); 612 613 multi->startQuery(query); 614 return; 615 } 616 completionHandler(intermediateResponse); 617 } 618 619 } // namespace query_param 620 } // namespace redfish 621